-----------------------------------------
--                                     --
--     W o w h e a d   L o o t e r     --
--                                     --
--                                     --
--    Patch: 4.2.0                     --
--    Updated: June 28, 2011           --
--    E-mail: feedback@wowhead.com     --
--                                     --
-----------------------------------------

WL_NAME = "Wowhead Looter";
WL_VERSION = 40018;
WL_VERSION_PATCH = 0;


-- SavedVariables
wlVersion, wlUploaded, wlStats, wlExportData, wlRealmList = 0, 0, "", "", {};
wlAuction, wlEvent, wlItemSuffix, wlObject, wlProfile, wlUnit = {}, {}, {}, {}, {}, {};
wlGlobalSetting = {};

-- SavedVariablesPerCharacter
wlSetting = {};
wlScans = {
	guid = nil,
	quests = {},
	recipes = "",
	profs = {},
	pets = { MOUNT = "", CRITTER = "" },
	glyphs = "",
	skills = "",
	titles = "",
	currencies = {},
	archaeology = {},
	timePlayerTotal = 0,
	timePlayedWhen = nil,
};
wlForgeSpells = {};
wlAnvilSpells = {};

WL_FORGE_ID = 1685;
WL_ANVIL_ID = 192628;

-- API

local _G = getfenv(0);
local CanMerchantRepair = _G.CanMerchantRepair;
local CastSpellByName = _G.CastSpellByName;
local CheckInteractDistance = _G.CheckInteractDistance;
local CloseTradeSkill = _G.CloseTradeSkill;
local CollapseQuestHeader = _G.CollapseQuestHeader;
local CollapseFactionHeader = _G.CollapseFactionHeader;
local CreateFrame = _G.CreateFrame;
local DungeonUsesTerrainMap = _G.DungeonUsesTerrainMap;
local ExpandFactionHeader = _G.ExpandFactionHeader;
local ExpandSkillHeader = _G.ExpandSkillHeader;
local ExpandTradeSkillSubClass = _G.ExpandTradeSkillSubClass;
local ExpandQuestHeader = _G.ExpandQuestHeader;
local GetAchievementCriteriaInfo = _G.GetAchievementCriteriaInfo;
local GetAuctionItemLink = _G.GetAuctionItemLink;
local GetAuctionItemInfo = _G.GetAuctionItemInfo;
local GetAccountExpansionLevel = _G.GetAccountExpansionLevel;
local GetBuildInfo = _G.GetBuildInfo;
local GetCVar = _G.GetCVar;
local GetContainerItemLink = _G.GetContainerItemLink;
local GetCompanionInfo = _G.GetCompanionInfo;
local GetCurrentMapDungeonLevel = _G.GetCurrentMapDungeonLevel;
local GetCursorPosition = _G.GetCursorPosition;
local GetFramesRegisteredForEvent = _G.GetFramesRegisteredForEvent;
local GetFactionInfo = _G.GetFactionInfo;
local GetGlyphInfo = _G.GetGlyphInfo;
local GetItemCount = _G.GetItemCount;
local GetItemInfo = _G.GetItemInfo;
local GetInstanceInfo = _G.GetInstanceInfo;
local GetLootSlotInfo = _G.GetLootSlotInfo;
local GetLootSlotLink = _G.GetLootSlotLink;
local GetLocale = _G.GetLocale;
local GetLanguageByIndex = _G.GetLanguageByIndex;
local GetMerchantItemLink = _G.GetMerchantItemLink;
local GetMerchantItemCostItem = _G.GetMerchantItemCostItem;
local GetMerchantItemCostInfo = _G.GetMerchantItemCostInfo;
local GetMerchantItemInfo = _G.GetMerchantItemInfo;
local GetMerchantNumItems = _G.GetMerchantNumItems;
local GetNumLanguages = _G.GetNumLanguages;
local GetNumFactions = _G.GetNumFactions;
local GetNumGlyphs = _G.GetNumGlyphs;
local GetNumRaidMembers = _G.GetNumRaidMembers;
local GetNumPartyMembers = _G.GetNumPartyMembers;
local GetNumTitles = _G.GetNumTitles;
local GetNumSkillLines = _G.GetNumSkillLines;
local GetNumLootItems = _G.GetNumLootItems;
local GetNumCompanions = _G.GetNumCompanions;
local GetNumTrainerServices = _G.GetNumTrainerServices;
local GetNumTradeSkills = _G.GetNumTradeSkills;
local GetNetStats = _G.GetNetStats;
local GetNumQuestLogEntries = _G.GetNumQuestLogEntries;
local GetNumDungeonMapLevels = _G.GetNumDungeonMapLevels;
local GetNumQuestLeaderBoards = _G.GetNumQuestLeaderBoards;
local GetPetActionInfo = _G.GetPetActionInfo;
local GetPlayerMapPosition = _G.GetPlayerMapPosition;
local GetProfessions = _G.GetProfessions;
local GetProfessionInfo = _G.GetProfessionInfo;
local GetProgressText = _G.GetProgressText;
local GetQuestID = _G.GetQuestID;
local GetQuestLink = _G.GetQuestLink;
local GetQuestsCompleted = _G.GetQuestsCompleted;
local GetQuestLogPushable = _G.GetQuestLogPushable;
local GetQuestLogTitle = _G.GetQuestLogTitle;
local GetQuestLogSelection = _G.GetQuestLogSelection;
local GetQuestLogTimeLeft = _G.GetQuestLogTimeLeft;
local GetQuestLogLeaderBoard = _G.GetQuestLogLeaderBoard;
local GetRealZoneText = _G.GetRealZoneText;
local GetRealmName = _G.GetRealmName;
local GetRewardText = _G.GetRewardText;
local GetSpellInfo = _G.GetSpellInfo;
local GetSkillLineInfo = _G.GetSkillLineInfo;
local GetTradeSkillInvSlots = _G.GetTradeSkillInvSlots;
local GetTradeSkillInvSlotFilter = _G.GetTradeSkillInvSlotFilter;
local GetTradeSkillLine = _G.GetTradeSkillLine;
local GetTradeSkillSubClassFilter = _G.GetTradeSkillSubClassFilter;
local GetTitleText = _G.GetTitleText;
local GetTradeSkillRecipeLink = _G.GetTradeSkillRecipeLink;
local GetTradeSkillInfo = _G.GetTradeSkillInfo;
local GetTradeSkillSubClasses = _G.GetTradeSkillSubClasses;
local GetTrainerServiceSkillReq = _G.GetTrainerServiceSkillReq;
local GetTrainerServiceCost = _G.GetTrainerServiceCost;
local GetTrainerServiceTypeFilter = _G.GetTrainerServiceTypeFilter;
local GetTrainerServiceInfo = _G.GetTrainerServiceInfo;
local IsAddOnLoaded = _G.IsAddOnLoaded;
local IsEquippedItem = _G.IsEquippedItem;
local IsFishingLoot = _G.IsFishingLoot;
local IsGlyphFlagSet = _G.IsGlyphFlagSet;
local IsTitleKnown = _G.IsTitleKnown;
local IsTradeSkillGuild = _G.IsTradeSkillGuild;
local IsTradeSkillLinked = _G.IsTradeSkillLinked;
local IsTradeskillTrainer = _G.IsTradeskillTrainer;
local IsUsableSpell = _G.IsUsableSpell;
local ItemTextGetItem = _G.ItemTextGetItem;
local LoadAddOn = _G.LoadAddOn;
local LootSlotIsItem = _G.LootSlotIsItem;
local LootSlotIsCoin = _G.LootSlotIsCoin;
local LootSlotIsCurrency = _G.LootSlotIsCurrency;
local QueryQuestsCompleted = _G.QueryQuestsCompleted;
local ReloadUI = _G.ReloadUI;
local RequestTimePlayed = _G.RequestTimePlayed;
local SelectQuestLogEntry = _G.SelectQuestLogEntry;
local SendAddonMessage = _G.SendAddonMessage;
local SetCVar = _G.SetCVar;
local SetDungeonMapLevel = _G.SetDungeonMapLevel;
local SetGlyphNameFilter = _G.SetGlyphNameFilter;
local SetGlyphFilter = _G.SetGlyphFilter;
local SetMapToCurrentZone = _G.SetMapToCurrentZone;
local SetTradeSkillSubClassFilter = _G.SetTradeSkillSubClassFilter;
local SetTradeSkillInvSlotFilter = _G.SetTradeSkillInvSlotFilter;
local SetTrainerServiceTypeFilter = _G.SetTrainerServiceTypeFilter;
local ToggleGlyphFilter = _G.ToggleGlyphFilter;
local TradeSkillOnlyShowMakeable = _G.TradeSkillOnlyShowMakeable;
local UnitSex = _G.UnitSex;
local UnitGUID = _G.UnitGUID;
local UnitIsTapped = _G.UnitIsTapped;
local UnitIsTappedByPlayer = _G.UnitIsTappedByPlayer;
local UnitManaMax = _G.UnitManaMax;
local UnitReaction = _G.UnitReaction;
local UnitPowerType = _G.UnitPowerType;
local UnitName = _G.UnitName;
local UnitRace = _G.UnitRace;
local UnitLevel = _G.UnitLevel;
local UnitIsDead = _G.UnitIsDead;
local UnitIsPlayer = _G.UnitIsPlayer;
local UnitHealthMax = _G.UnitHealthMax;
local UnitIsTrivial = _G.UnitIsTrivial;
local UnitIsFriend = _G.UnitIsFriend;
local UnitExists = _G.UnitExists;
local UnitClass = _G.UnitClass;
local UnitExists = _G.UnitExists;
local UnitFactionGroup = _G.UnitFactionGroup;
local UnitPlayerControlled = _G.UnitPlayerControlled;
local UnitClassification = _G.UnitClassification;

--Archaeology API
local IsArtifactCompletionHistoryAvailable = _G.IsArtifactCompletionHistoryAvailable;
local RequestArtifactCompletionHistory = _G.RequestArtifactCompletionHistory;
local GetArchaeologyRaceInfo = _G.GetArchaeologyRaceInfo;
local GetArtifactInfoByRace = _G.GetArtifactInfoByRace;
local GetNumArchaeologyRaces = _G.GetNumArchaeologyRaces;
local GetNumArtifactsByRace = _G.GetNumArtifactsByRace;
local GetActiveArtifactByRace = _G.GetActiveArtifactByRace;

-- Hooks
local wlDefaultGetQuestReward;
local wlDefaultChatFrame_DisplayTimePlayed;

-- Local Variables
local wlTracker = {};
local wlNpcInfo = {};
local wlLootCooldown = {};
local wlFaction = {};
local wlCurrentMindControlTarget = nil;
local wlTimers = {};
local wlMessages = {};
local wlMsgCollected = nil;
local wlUserCollected = false;

local wlId = nil;
local wlN = 0;
local wlEventId = 1;

local WL_SPELL_BLACKLIST = {
	[0] = true,     -- ??
	[1604] = true,  -- Dazed
	[1] = true,     -- Word of Recall (OLD)
	[4] = true,     -- Word of Recall Other
}

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetNextEventId()
	if not wlEvent or not wlEvent[wlId] or not wlEvent[wlId][wlN] or not wlEvent[wlId][wlN][wlEventId] then
		return wlEventId;
	else
		wlEventId = wlEventId + 1;
		return wlEventId;
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_PLAYER_LOGIN(self)
	if wlVersion ~= WL_VERSION or wlUploaded ~= 0 then
		wlReset();
	end

	wlHook();
	wlClearTracker("quest", "rep", "spell");

	wlRealmList[GetCVar("realmList") or "nil"] = 1;

	wlId = wlConcat(wlSelectOne(1, UnitName("player")), GetRealmName());
	
	-- Only add the data to the wlProfile if the character is level 10 or above
	if UnitLevel("player") > 10 then
		wlUpdateVariable(wlProfile, wlId, "init", {
			faction = UnitFactionGroup("player"),
			race = select(2, UnitRace("player")),
			class = select(2, UnitClass("player")),
			sex = UnitSex("player"),
			n = 0,
		});
		wlN = wlUpdateVariable(wlProfile, wlId, "n", "add", 1);
	end
	
	wlUpdateVariable(wlEvent, wlId, wlN, wlGetNextEventId(), "set", {
		what = "login",
		date = wlConcat(wlGetDate()),
	});

	-- Restore settings
	if wlSetting.locMap then
		wlLocMapFrame:Show();
	end

	if wlSetting.locTooltip then
		wlLocTooltipFrame:Show();
	end

	if wlSetting.idTooltip then
		wlIdTooltipFrame:Show();
	end

	wlNumQuestCompleted = wlGetNumLoremasterQuestCompleted();

	-- We're not going to use historical data for the uploads anymore
	-- If a field is "" in the upload, the server will not touch
	--  the database for that field.  (i.e. we didn't scan that data)
	-- If a field is "-1", it means we've scanned for the data, but
	--  there is none (i.e. they don't have any mounts, for example)
	--  The server will wipe any data it had, and replace it with nothing
	-- Otherwise, the field will contain ALL the pertinent data, so the
	--  server wipes what it had and replaces it with what's in the upload

	if wlScans.guid == UnitGUID("player") then
		wlScans.quests = {};
		wlScans.recipes = "";
		wlScans.profs = {};
		wlScans.glyphs = "";
		wlScans.skills = "";
		wlScans.titles = "";
		wlScans.currencies = {};
		wlScans.archaeology = {};
		wlScans.pets = {};
		wlScans.pets.MOUNT = "";
		wlScans.pets.CRITTER = "";
		wlScans.timePlayerTotal = 0;
		wlScans.timePlayedWhen = nil;
	else
		wlScans.guid = UnitGUID("player");
		wlScans.quests = {};
		wlScans.recipes = "";
		wlScans.profs = {};
		wlScans.glyphs = "";
		wlScans.skills = "";
		wlScans.titles = "";
		wlScans.currencies = {};
		wlScans.archaeology = {};
		wlScans.pets = {};
		wlScans.pets.MOUNT = "";
		wlScans.pets.CRITTER = "";
		wlScans.timePlayerTotal = 0;
		wlScans.timePlayedWhen = nil;
	end

	wlScanCurrencies()

	wlMessage(WL_LOADED:format(WL_NAME, WL_VERSION), true);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_PLAYER_LOGOUT(self)
	wlRefreshExportData();
	wlUnhook();
	wlRegisterStats();
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_ADDON_LOADED(self)
-- This will reset the auto collect timer until the last add-on is loaded
	wlTimers.autoCollect = wlGetTime() + 60000; -- 60 seconds timeout
end

--**--**--**--**--**--**--**-- **--**--**--**--**--**--**--**--**--**--**--



-------------------------
-------------------------
--                     --
--   UNIT  FUNCTIONS   --
--                     --
-------------------------
-------------------------

function wlUnitName(unit)
	local name = UnitName(unit);

	if wlIsValidName(name) then
		return name;
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlUnitGUID(unit)
	local name = wlUnitName(unit);
	local id, kind = wlParseGUID(UnitGUID(unit));

	if name and id and kind == "npc" then
		wlUpdateVariable(wlNpcInfo, name, "time", "set", wlGetTime());
		wlUpdateVariable(wlNpcInfo, name, "id", "set", id);
		wlUpdateVariable(wlNpcInfo, name, "isTrivial", "set", UnitIsTrivial(unit));
	end

	return id, kind;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_PLAYER_TARGET_CHANGED(self)
	if not UnitExists("target") or UnitPlayerControlled("target") then
		return;
	end

	local id, kind = wlUnitGUID("target");

	if not id or kind ~= "npc" then
		return;
	end

	wlUpdateVariable(wlUnit, id, "init", {
		class = select(2, UnitClass("target")),
		isPvp = UnitIsPVP("target") and true or false,
	});
	
	if not wlUnit[id].class then
		wlUpdateVariable(wlUnit, id, "class", "set", select(2, UnitClass("target")));
		wlUpdateVariable(wlUnit, id, "isPvp", "set", UnitIsPVP("target") and true or false);
	end

	wlUpdateVariable(wlUnit, id, "sex", UnitSex("target"), "add", 1);

	if not wlUnit[id].faction then
		wlUnit[id].faction = wlUnitFaction("target");
	end

	wlUpdateVariable(wlUnit, id, "reaction", wlConcat(UnitLevel("player"), UnitFactionGroup("player"), UnitReaction("player", "target")), "init", wlGetTime());

	local dd, level = wlGetInstanceDifficulty(), UnitLevel("target");

	wlUpdateVariable(wlUnit, id, "spec", dd, level, "init", {
		health = UnitHealthMax("target"),
		powermax = UnitManaMax("target"),
		powertype = UnitPowerType("target"),
	});
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlUnitIsClose(unit)
	return CheckInteractDistance(unit, 1) and CheckInteractDistance(unit, 2) and CheckInteractDistance(unit, 3) and CheckInteractDistance(unit, 4);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRegisterUnitLocation(id, level)
	local dd = wlGetInstanceDifficulty();
	local zone, x, y, dl = wlGetLocation();

	wlUpdateVariable(wlUnit, id, "spec", dd, level, "loc", zone, "init", { n = 0 });

	local i = wlGetLocationIndex(wlUnit[id].spec[dd][level].loc[zone], x, y, dl, 5);
	if i then
		local n = wlUnit[id].spec[dd][level].loc[zone][i].n;

		wlUnit[id].spec[dd][level].loc[zone][i].x = math.floor((wlUnit[id].spec[dd][level].loc[zone][i].x * n + x) / (n + 1) + 0.5);
		wlUnit[id].spec[dd][level].loc[zone][i].y = math.floor((wlUnit[id].spec[dd][level].loc[zone][i].y * n + y) / (n + 1) + 0.5);
		wlUnit[id].spec[dd][level].loc[zone][i].n = n + 1;
	else
		i = wlUpdateVariable(wlUnit, id, "spec", dd, level, "loc", zone, "n", "add", 1);
		wlUpdateVariable(wlUnit, id, "spec", dd, level, "loc", zone, i, "set", {
			x = x,
			y = y,
			dl = dl,
			n = 1,
		});
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CHAT_MSG_MONSTER_SAY(self, text, name, language)
	wlRegisterUnitQuote(name, "say", language, text);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CHAT_MSG_MONSTER_WHISPER(self, text, name, language)
	wlRegisterUnitQuote(name, "whisper", language, text);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CHAT_MSG_MONSTER_YELL(self, text, name, language)
	wlRegisterUnitQuote(name, "yell", language, text);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlReplacePlayerName(name)
	if name == UnitName("player") then
		return "<name>";
	end
end

local wlLanguage = nil;
function wlRegisterUnitQuote(name, how, language, text)
-- Init
	text = text:gsub("(%w+)", wlReplacePlayerName);

	if not wlLanguage then
		wlLanguage = {};

		for i=1,GetNumLanguages() do
			wlLanguage[GetLanguageByIndex(i)] = 1;
		end
	end

	-- language -> language known
	if (language ~= "" and not wlLanguage[language]) or not name or not wlNpcInfo[name] or not wlUnit[wlNpcInfo[name].id] then
		return;
	end

	wlUpdateVariable(wlUnit, wlNpcInfo[name].id, "quote", how, text, "set", language);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_GOSSIP_SHOW(self)
	local gossips = { GetGossipOptions() };

	for i=1,#gossips,2 do
		if gossips[i + 1] ~= "gossip" then
			wlRegisterUnitGossip(gossips[i + 1]);
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_AUCTION_HOUSE_SHOW(self)
	wlRegisterUnitGossip("auctioneer");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_BANKFRAME_OPENED(self)
	wlRegisterUnitGossip("banker");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_BATTLEFIELDS_SHOW(self)
	wlRegisterUnitGossip("battlemaster");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CONFIRM_BINDER(self)
	wlRegisterUnitGossip("binder");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CONFIRM_PET_UNLEARN(self)
	wlRegisterUnitGossip("pettrainer");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CONFIRM_TALENT_WIPE(self)
	wlRegisterUnitGossip("talentwiper");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_GOSSIP_ENTER_CODE(self, codeId)
	wlRegisterUnitGossip("code" .. codeId);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_OPEN_TABARD_FRAME(self)
	wlRegisterUnitGossip("tabard");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_PET_STABLE_SHOW(self)
	wlRegisterUnitGossip("stable");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_TAXIMAP_OPENED(self)
	wlRegisterUnitGossip("taxi");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRegisterUnitGossip(gossip)
	if not gossip or gossip == "" then
		return;
	end

	local id, kind = wlUnitGUID("npc");

	if not id or kind ~= "npc" or not wlUnit[id] then
		return;
	end

	wlUpdateVariable(wlUnit, id, "gossip", gossip, "add", 1);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_PET_BAR_UPDATE(self)
	wlCurrentMindControlTarget = nil;
	if wlTracker.spell and wlTracker.spell.time and wlTracker.spell.action == "MindControl" and wlTracker.spell.event == "SUCCEEDED" then
		local id, kind = wlUnitGUID("pet");

		if id and kind == "npc" and wlUnit[id] and id == wlTracker.spell.id then
			wlCurrentMindControlTarget = id;
		end

		wlClearTracker("spell");
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_MERCHANT_SHOW(self)
	wlRegisterUnitGossip("vendor");
	wlEvent_MERCHANT_UPDATE(self);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_MERCHANT_UPDATE(self)
	local id, kind = wlUnitGUID("npc");

	if not id or kind ~= "npc" or not wlUnit[id] then
		return;
	end

	if CanMerchantRepair() then
		wlUpdateVariable(wlUnit, id, "canRepair", "init", 1);
	end

	local standing = select(2, wlUnitFaction("npc"));

	local merchantItemList = {};

	for slot=1,GetMerchantNumItems() do
		local _, _, price, stack, numAvailable, _, extendedCost = GetMerchantItemInfo(slot);
		local id, subId = wlParseItemLink(GetMerchantItemLink(slot));
		if id ~= 0 then
			price = wlGetFullCost(price, standing);

			if extendedCost then
				local personalRating, battlegroundRating = 0, 0;

				wlGameTooltip:ClearLines();
				wlGameTooltip:SetMerchantItem(slot);

				for i=2,wlGameTooltip:NumLines() do
					local line = getglobal("wlGameTooltipTextLeft" .. i);

					if not line then
						break;
					end

					local pts = wlParseString("^" .. ITEM_REQ_ARENA_RATING .. "$", line:GetText());
					if pts then
						personalRating = pts;
						break;
					end

					local pts = wlParseString("^" .. ITEM_REQ_ARENA_RATING_3V3 .. "$", line:GetText());
					if pts then
						personalRating = pts + 1000000; -- 3v3 or 5v5 flag
						break;
					end

					local pts = wlParseString("^" .. ITEM_REQ_ARENA_RATING_5V5 .. "$", line:GetText());
					if pts then
						personalRating = pts + 2000000; -- 5v5 flag
						break;
					end

					if type(ITEM_REQ_ARENA_RATING_BG) == "string" and type(ITEM_REQ_ARENA_RATING_3V3_BG) == "string" then
						local pts1, pts2 = wlParseString("^" .. ITEM_REQ_ARENA_RATING_BG .. "$", line:GetText());
						if pts1 and pts2 then
							personalRating = pts1;
							battlegroundRating = pts2;
							break;
						end

						local pts1, pts2 = wlParseString("^" .. ITEM_REQ_ARENA_RATING_3V3_BG .. "$", line:GetText());
						if pts1 and pts2 then
							personalRating = pts1 + 1000000; -- 3v3 or 5v5 flag
							battlegroundRating = pts2;
							break;
						end
					end
				end

				extendedCost = personalRating .. "#" .. battlegroundRating;

				local merchantCurrencies = {};

				for i=1,GetMerchantItemCostInfo(slot) do

					local currencyId;
					local texture, qty, link = GetMerchantItemCostItem(slot, i);

					if link then
						currencyId = wlParseItemLink(link);
					end

					if not currencyId or currencyId == 0 then
						wlGameTooltip:ClearLines();
						wlGameTooltip:SetMerchantCostItem(slot, i);

						if not texture or not wlGameTooltipTextLeft1:GetText() or wlGameTooltipTextLeft1:GetText() == "" then
							return; -- error
						end

						-- Create a unique ID using the icon name and the currency localized name
						currencyId = texture:match("[^\\]+$"):lower() .. "<>" .. wlGameTooltipTextLeft1:GetText();
					end

					table.insert(merchantCurrencies, { qty, currencyId });
				end

				table.sort(merchantCurrencies,function(l, r) return l[1] < r[1] end);

				for _,currency in ipairs(merchantCurrencies) do
					extendedCost = extendedCost .. "#" .. currency[1] .. "<>" .. currency[2];
				end

				price = wlConcat(price, extendedCost);
			end

			merchantItemList[wlConcat(id, subId, stack, price)] = numAvailable;
		end
	end

	for link,numAvailable in pairs(merchantItemList) do
		wlUpdateVariable(wlUnit, id, "merchant", link, "max", numAvailable);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_TRAINER_SHOW(self)
	wlRegisterUnitGossip("trainer");

	local id, kind = wlUnitGUID("npc");

	if not id or kind ~= "npc" or not wlUnit[id] then
		return;
	end

	local fAvail, fUnavail, fUsed = GetTrainerServiceTypeFilter("available"), GetTrainerServiceTypeFilter("unavailable"), GetTrainerServiceTypeFilter("used");
	SetTrainerServiceTypeFilter("available", 1);
	SetTrainerServiceTypeFilter("unavailable", 1);
	SetTrainerServiceTypeFilter("used", 1);

	local standing = select(2, wlUnitFaction("npc"));
	local isTradeSkill = IsTradeskillTrainer();
	local spellId, skill, skillRank, cost;

	for i=1,GetNumTrainerServices() do
		local spellName, spellSubText, _, _, reqLevel = GetTrainerServiceInfo(i);

		if spellName then
			wlGameTooltip:SetTrainerService(i);
			spellId = select(3, wlGameTooltip:GetSpell());

			if isTradeSkill then
				skill, skillRank = GetTrainerServiceSkillReq(i);
				skill, skillRank = skill or "", skillRank or 0;
			else
				skill, skillRank = "", 0;
			end

			cost = wlGetFullCost(GetTrainerServiceCost(i), standing);

			wlUpdateVariable(wlUnit, id, "training", wlSelectOne(2, UnitClass("player")), wlConcat(spellId, skill, skillRank, reqLevel, cost), "init", 1);
		end
	end

	SetTrainerServiceTypeFilter("available", fAvail or 0);
	SetTrainerServiceTypeFilter("unavailable", fUnavail or 0);
	SetTrainerServiceTypeFilter("used", fUsed or 0);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CHAT_MSG_MONSTER_EMOTE(self, emote, name)
	if emote == WL_RUNSAWAY then
		if not name or not wlNpcInfo[name] or not wlUnit[wlNpcInfo[name].id] then
			return;
		end

		wlUpdateVariable(wlUnit, wlNpcInfo[name].id, "runsaway", "init", 1);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- Support PTR API change in 4.2
function wlEvent_COMBAT_LOG_EVENT_UNFILTERED_40200(self, timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...)
	wlEvent_COMBAT_LOG_EVENT_UNFILTERED(self, timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlNpcFlags = bit.bor(COMBATLOG_OBJECT_TYPE_NPC, COMBATLOG_OBJECT_TYPE_GUARDIAN, COMBATLOG_OBJECT_CONTROL_NPC, COMBATLOG_OBJECT_AFFILIATION_OUTSIDER);
local wlMindControlFlags = bit.bor(COMBATLOG_OBJECT_TYPE_PET, COMBATLOG_OBJECT_CONTROL_PLAYER, COMBATLOG_OBJECT_REACTION_FRIENDLY, COMBATLOG_OBJECT_AFFILIATION_MINE);
local wlMostRecentEliteKilled = {}

function wlEvent_COMBAT_LOG_EVENT_UNFILTERED(self, timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
	if event == "PARTY_KILL" then
		wlClearTracker("rep");
		
		local id = wlParseGUID(destGUID);
		
		-- npc check
		if not string.match(destGUID, "^0xF.[35]") or not id or id == 0 or not wlUnit[id] then
			return;
		end
		
		-- critter check
		wlGameTooltip:ClearLines();
		wlGameTooltip:SetHyperlink("unit:"..destGUID);
		for line=2, wlGameTooltip:NumLines() do
			local creatureType = getglobal("wlGameTooltipTextLeft"..line):GetText();
			if creatureType:match(WL_CRITTER) then
				return;
			end
		end

		local now = wlGetTime();
		
		if wlNpcInfo[destName] and wlNpcInfo[destName].id == id and wlIsValidInterval(wlNpcInfo[destName].time, now, 15000) then
			wlTracker.rep.time = now;
			wlTracker.rep.id = id;
			wlTracker.rep.flags = wlNpcInfo[destName].isTrivial and 1 or 0;
		end

	elseif event == "SPELL_CAST_SUCCESS" and sourceGUID == UnitGUID("player") and select(1, ...) == 30427 then
		wlClearTracker("spell");
		wlTracker.spell.time = wlGetTime();
		wlTracker.spell.action = 30427;

	elseif event == "SPELL_CAST_START" or event == "SPELL_CAST_SUCCESS" or event == "SPELL_AURA_APPLIED" then
		local unitId = wlParseGUID(sourceGUID);
		local spellId, spellName = ...;
		
		-- if spell is blacklisted (mainly dazed)
		if not spellId or WL_SPELL_BLACKLIST[spellId] then
			return;
		end
		
		-- npc check
		if not string.match(sourceGUID, "^0xF.[35]") or not unitId or unitId == 0 then
			return;
		end
		
		if bit.band(wlNpcFlags, sourceFlags) ~= 0 then

			wlUpdateVariable(wlUnit, unitId, "spec", wlGetInstanceDifficulty(), "spell", spellId, "add", 1);

		elseif bit.band(wlMindControlFlags, sourceFlags) ~= 0 and wlCurrentMindControlTarget == id  and wlUnit[unitId] then

			for i=1,NUM_PET_ACTION_SLOTS do
				local petSpellName, _, _, _, autoCastAllowed = GetPetActionInfo(i);

				if petSpellName == spellName then
					wlUpdateVariable(wlUnit, unitId, "spec", wlGetInstanceDifficulty(), "mcspell", i, "init", wlConcat(spellId, autoCastAllowed or 0));
					break;
				end
			end
		end

	elseif event == "UNIT_DISSIPATES" then
	-- Register cloud's location
		if wlTracker.spell.action == 30427 and wlIsValidInterval(wlTracker.spell.time or 0, wlGetTime(), 5000) then
			local unitId = wlParseGUID(destGUID);
			wlRegisterUnitLocation(unitId, 1);

			wlClearTracker("spell");
		end

	elseif event == "UNIT_DIED" then
	-- if the unit is your target
		local currentTargetGUID = UnitGUID("target")
		if currentTargetGUID == destGUID then
		-- if the unit !normal and !trivial
			local classification = UnitClassification("target")
			if classification ~= "normal" and classification ~= "trivial" then
				local id = wlUnitGUID("target")
				wlMostRecentEliteKilled = { guid = destGUID, name = destName, id = id, timeOfDeath = wlGetTime() }
			end
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



----------------------------
----------------------------
--                        --
--   FACTION  FUNCTIONS   --
--                        --
----------------------------
----------------------------

local wlEvent_UPDATE_FACTION_running = false;
function wlEvent_UPDATE_FACTION(self)
	if wlEvent_UPDATE_FACTION_running then
		return;
	end

	wlEvent_UPDATE_FACTION_running = true;
	self:UnregisterEvent("UPDATE_FACTION");

	local wlCollapsedHeaders = {};

	local i = 1;
	while i <= GetNumFactions() do
		local name, _, standing, _, _, _, _, _, isHeader, isCollapsed = GetFactionInfo(i);

		if name then
			if isHeader then
				if isCollapsed then
					ExpandFactionHeader(i);
					wlCollapsedHeaders[name] = 1;
				end
			else
				wlFaction[name] = standing;
			end
		end

		i = i + 1;
	end

	-- Restore headers
	i = 1;
	while i <= GetNumFactions() do
		local name, _, _, _, _, _, _, _, isHeader = GetFactionInfo(i);

		if name and isHeader and wlCollapsedHeaders[name] then
			CollapseFactionHeader(i);
		end

		i = i + 1;
	end

	self:RegisterEvent("UPDATE_FACTION");
	wlEvent_UPDATE_FACTION_running = false;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlUnitFaction(unit)
	wlGameTooltip:ClearLines();
	wlGameTooltip:SetUnit(unit);

	for line=2,wlGameTooltip:NumLines() do
		local faction = getglobal("wlGameTooltipTextLeft" .. line):GetText();
		if wlFaction[faction] then
			return faction, wlFaction[faction];
		end
	end

	return nil, nil;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



---------------------------
---------------------------
--                       --
--   OBJECT  FUNCTIONS   --
--                       --
---------------------------
---------------------------

function wlEvent_ITEM_TEXT_BEGIN(self)
	local id, kind = wlUnitGUID("npc");

	if wlUnitName("npc") == ItemTextGetItem() and id and kind == "object" then
		wlRegisterObject(id);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_MAIL_SHOW(self)
	wlRegisterObject(wlUnitGUID("npc"));
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRegisterObject(id)
	if not id then
		return;
	end

	local zone, x, y, dl = wlGetLocation();

	zone = wlConcat(wlGetInstanceDifficulty(), zone);

	wlUpdateVariable(wlObject, id, zone, "init", { n = 0 });

	local i = wlGetLocationIndex(wlObject[id][zone], x, y, dl, 5);
	if i then
		local n = wlObject[id][zone][i].n;

		wlObject[id][zone][i].x = math.floor((wlObject[id][zone][i].x * n + x) / (n + 1) + 0.5);
		wlObject[id][zone][i].y = math.floor((wlObject[id][zone][i].y * n + y) / (n + 1) + 0.5);
		wlObject[id][zone][i].n = n + 1;
	else
		i = wlUpdateVariable(wlObject, id, zone, "n", "add", 1);
		wlUpdateVariable(wlObject, id, zone, i, "set", {
			x = x,
			y = y,
			dl = dl,
			n = 1,
		});
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



--------------------------
--------------------------
--                      --
--   QUEST  FUNCTIONS   --
--                      --
--------------------------
--------------------------

local wlQuestLog, wlQuestObjectives, wlCurrentQuestObj = {}, { {}, {} }, 1;
local wlNumQuestCompleted = -1;

local wlEvent_QUEST_LOG_UPDATE_running = false;
function wlEvent_QUEST_LOG_UPDATE(self)
	if wlEvent_QUEST_LOG_UPDATE_running or not wlEvent or not wlId or not wlEvent[wlId] or not wlN or not wlEvent[wlId][wlN] then
		return;
	end

	wlEvent_QUEST_LOG_UPDATE_running = true;

	wlGlobalUnregister("QUEST_LOG_UPDATE");

	local eventId = wlGetNextEventId();

	local selectedQuest = GetQuestLogSelection();
	local wlCollapsedHeaders = {};

	-- "erase" all
	for k,_ in ipairs(wlQuestLog) do
		wlQuestLog[k].id = nil;
	end

	wlCurrentQuestObj = 3 - wlCurrentQuestObj; -- toggle: 1 <-> 2

	for k,_ in ipairs(wlQuestObjectives[wlCurrentQuestObj]) do
		wlQuestObjectives[wlCurrentQuestObj][k].questId = nil;
	end


	local i, cQ, cO = 1, 1, 1;
	while i <= GetNumQuestLogEntries() do
		local title, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(i);

		if title then
			if isHeader then
				if isCollapsed then
					ExpandQuestHeader(i);
					wlCollapsedHeaders[title] = 1;
				end
			else
				SelectQuestLogEntry(i);
				local questId = wlSelectOne(1, wlParseQuestLink(GetQuestLink(i)));

				wlUpdateVariable(wlQuestLog, cQ, "title", "set", title);

				wlQuestLog[cQ].id = questId;
				wlQuestLog[cQ].timer = math.ceil((GetQuestLogTimeLeft() or 0) / 15) * 15;
				wlQuestLog[cQ].sharable = GetQuestLogPushable() and 1 or 0;

				cQ = cQ + 1;

				for objId=1,GetNumQuestLeaderBoards() do
					local _, kind, done = GetQuestLogLeaderBoard(objId);

					if kind == "item" or kind == "event" then
						wlUpdateVariable(wlQuestObjectives, wlCurrentQuestObj, cO, "questId", "set", questId);

						wlQuestObjectives[wlCurrentQuestObj][cO].objId = objId;
						wlQuestObjectives[wlCurrentQuestObj][cO].done = done;

						cO = cO + 1;

						local index = wlTableFind(wlQuestObjectives[3 - wlCurrentQuestObj],function(a, questId, objId) return a.questId == questId and a.objId == objId; end, questId, objId)
						if (index and wlQuestObjectives[3 - wlCurrentQuestObj][index].done ~= done) or (not index and not done) then
							wlUpdateVariable(wlEvent, wlId, wlN, eventId, "initArray", 0);
							wlEvent[wlId][wlN][eventId].what = "questStatus";
							wlEvent[wlId][wlN][eventId][wlConcat(questId, objId, done or 0)] = done and kind == "event" and wlConcat(wlGetLocation()) or 1;
						end
					end
				end

			end
		end

		i = i + 1;
	end

	-- Restore headers
	i = 1;
	while i <= GetNumQuestLogEntries() do
		local title, _, _, _, isHeader = GetQuestLogTitle(i);

		if title and isHeader and wlCollapsedHeaders[title] then
			CollapseQuestHeader(i);
		end

		i = i + 1;
	end

	SelectQuestLogEntry(selectedQuest);

	wlGlobalRegister("QUEST_LOG_UPDATE");

	-- abandoned quest
	for k,v in ipairs(wlQuestObjectives[3 - wlCurrentQuestObj]) do
		if v.questId and not v.done then
			local index = wlTableFind(wlQuestObjectives[wlCurrentQuestObj],function(a, questId, objId) return a.questId == questId and a.objId == objId; end, v.questId, v.objId)
			if not index then
				wlUpdateVariable(wlEvent, wlId, wlN, eventId, "initArray", 0);
				wlEvent[wlId][wlN][eventId].what = "questStatus";
				wlEvent[wlId][wlN][eventId][wlConcat(v.questId, v.objId, 1)] = 1;
			end
		end
	end

	if wlTracker.quest.time and wlTracker.quest.action == "accept" then
		i = wlTableFind(wlQuestLog,function(a, v) return a.id and a.id == v; end, wlTracker.quest.id);
		if i and wlIsValidInterval(wlTracker.quest.time, wlGetTime(), 5000) then
			wlRegisterQuestAccept(i);
		end
	end

	wlEvent_QUEST_LOG_UPDATE_running = false;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlQuestAchievements = { 33, 34, 35, 36, 37, 38, 39, 40, 41, 1189, 1190, 1191, 1192, 1193, 1194, 1195, 1262, 1271, 1272, 1273, 1274, 1356, 1357, 1358, 1359, 1360, 1676, 1677, 1678, 1680, 4869, 4870, 4871, 4872, 4873, 4875, 4892, 4893, 4894, 4895, 4896, 4897, 4898, 4899, 4900, 4901, 4902, 4903, 4904, 4905, 4906, 4907, 4908, 4909, 4910, 4925, 4926, 4927, 4928, 4929, 4931, 4932, 4933, 4934, 4935, 4936, 4937, 4938, 4939, 4940, 4976, 4978, 4979, 4980, 4981, 4982, 4983, 5501 };
function wlGetNumLoremasterQuestCompleted()
	local nQuestCompleted = 0;

	for _,id in ipairs(wlQuestAchievements) do
		nQuestCompleted = nQuestCompleted + (wlSelectOne(4, GetAchievementCriteriaInfo(id, 1)) or 0);
	end

	return nQuestCompleted;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_QUEST_DETAIL(self)
	wlClearTracker("quest");

	local name, id, kind = wlUnitName("questnpc"), wlUnitGUID("questnpc");
	if not name or not id then
		return;
	end

	if kind == "object" then
		wlRegisterObject(id);
	end

	wlTracker.quest.time = wlGetTime();
	wlTracker.quest.action = "accept";
	wlTracker.quest.id = GetQuestID();
	wlTracker.quest.targetname = name;
	wlTracker.quest.targetkind = kind;
	wlTracker.quest.targetid = id;

-- ...Wait for the quest log refresh to register the quest
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_QUEST_PROGRESS(self)
	wlClearTracker("quest");

	wlTracker.quest.id = GetQuestID();
	wlTracker.quest.progress = wlGetSourceText(GetProgressText());

	wlTracker.quest.time = wlGetTime();
	wlTracker.quest.action = "progress";
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_QUEST_COMPLETE(self)
	if wlTracker.quest.action ~= "progress" or wlTracker.quest.id ~= GetQuestID() then
		wlClearTracker("quest"); -- Not the same quest

		wlTracker.quest.id = GetQuestID();
		wlTracker.quest.progress = "";
	end

	wlTracker.quest.complete = wlGetSourceText(GetRewardText());

	wlTracker.quest.time = wlGetTime();
	wlTracker.quest.action = "complete";
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetQuestReward(...) -- Turn-in
	local success, msg = pcall(wlRegisterQuestReturn);

	if not success then
		error(msg);
	end

	return wlDefaultGetQuestReward(...);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRegisterQuestAccept(index)
	if not wlEvent or not wlId or not wlEvent[wlId] or not wlN or not wlEvent[wlId][wlN] then
		return;
	end

	local eventId = wlGetNextEventId();

	wlUpdateVariable(wlEvent, wlId, wlN, eventId, "initArray", 0);

	wlEvent[wlId][wlN][eventId].what = "quest";
	wlEvent[wlId][wlN][eventId].action = "accept";

	wlEvent[wlId][wlN][eventId].timer = wlQuestLog[index].timer;
	wlEvent[wlId][wlN][eventId].sharable = wlQuestLog[index].sharable;

	wlEvent[wlId][wlN][eventId].questid = wlTracker.quest.id;
	wlEvent[wlId][wlN][eventId].targetname = wlTracker.quest.targetname;
	wlEvent[wlId][wlN][eventId].targetkind = wlTracker.quest.targetkind;
	wlEvent[wlId][wlN][eventId].targetid = wlTracker.quest.targetid;

	wlClearTracker("quest");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRegisterQuestReturn()
	if wlTracker.quest.action ~= "complete" then
		return;
	end

	local name, id, kind = wlUnitName("questnpc"), wlUnitGUID("questnpc");

	if not name or not id then
		return;
	end

	if kind == "object" then
		wlRegisterObject(id);
	end

	if not wlEvent or not wlId or not wlEvent[wlId] or not wlN or not wlEvent[wlId][wlN] then
		return;
	end

	local eventId = wlGetNextEventId();

	wlUpdateVariable(wlEvent, wlId, wlN, eventId, "initArray", 0);

	wlEvent[wlId][wlN][eventId].what = "quest";
	wlEvent[wlId][wlN][eventId].action = "turn-in";

	wlEvent[wlId][wlN][eventId].questid = wlTracker.quest.id;
	wlEvent[wlId][wlN][eventId].progress = wlTracker.quest.progress;
	wlEvent[wlId][wlN][eventId].complete = wlTracker.quest.complete;

	wlEvent[wlId][wlN][eventId].targetname = name;
	wlEvent[wlId][wlN][eventId].targetkind = kind;
	wlEvent[wlId][wlN][eventId].targetid = id;

	oldNumQuestCompleted, wlNumQuestCompleted = wlNumQuestCompleted, wlGetNumLoremasterQuestCompleted();

	if oldNumQuestCompleted ~= wlNumQuestCompleted then
		wlEvent[wlId][wlN][eventId].loremaster = 1;
	end
	
	wlScans.quests[wlTracker.quest.id] = true;

	wlTracker.quest.time = wlGetTime();
	wlTracker.quest.eventId = eventId;
	wlTracker.quest.action = "turn-in";
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_COMBAT_TEXT_UPDATE(self, messageType, param1, param2)
	if messageType == "FACTION" then
		if wlIsValidInterval(wlTracker.quest.time or 0, wlGetTime(), 1000) and wlTracker.quest.action == "turn-in" then
			return; -- Quest reputation
		elseif not wlIsValidInterval(wlTracker.rep.time or 0, wlGetTime(), 1000) or not wlFaction[param1] then
			return; -- Not kill reputation
		end

		if wlSelectOne(2, UnitRace("player")) == "Human" and param2 > 0 then -- Racial +10%
			param2 = math.floor(param2 / 1.1 + 0.5);
		end
		
		wlUpdateVariable(wlUnit, wlTracker.rep.id, "spec", wlGetInstanceDifficulty(), "rep", wlConcat(param1, wlFaction[param1], param2, wlTracker.rep.flags), "add", 1);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetInstanceDifficulty()
	local _, instanceType, instanceDifficulty, _, _, dynamicDifficulty = GetInstanceInfo();

	if type(dynamicDifficulty) ~= "nil" and dynamicDifficulty == 1 then
		if instanceDifficulty == 1 then
			instanceDifficulty = 3;
		elseif instanceDifficulty == 2 then
			instanceDifficulty = 4;
		end
	end

	if instanceType == "party" then
		return -instanceDifficulty;

	elseif instanceType == "raid" then
		return instanceDifficulty;

	else
		return 0;
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_QUEST_QUERY_COMPLETE(self, ...)
	GetQuestsCompleted(wlScans.quests);

	if wlTableIsEmpty(wlScans.quests) then
	-- Query quests again until something is collected
		wlTimers.quests = wlGetTime() + 30000; -- 30 seconds timeout
	else
		wlTimers.quests = nil;
		wlAppendMsgCollected(WL_COLLECT_QUESTS);
	end
end



-------------------------
-------------------------
--                     --
--   LOOT  FUNCTIONS   --
--                     --
-------------------------
-------------------------


--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- map icon_file_name to currency index
-- ALWAYS compare these case-insensitive!
local wlCurrencies = {
	["trade_archaeology_draenei_artifactfragment"] = 398,
	["trade_archaeology_dwarf_artifactfragment"] = 384,
	["trade_archaeology_fossil_fern"] = 393,
	["trade_archaeology_nerubian_artifactfragment"] = 400,
	["trade_archaeology_highborne_artifactfragment"] = 394,
	["trade_archaeology_orc_artifactfragment"] = 397,
	["trade_archaeology_titan_fragment"] = 401,
	["trade_archaeology_troll_artifactfragment"] = 385,
	["trade_archaeology_vrykul_artifactfragment"] = 399,
	["inv_misc_token_argentdawn3"] = 361,
	["pvecurrency-justice"] = 395,
	["pvecurrency-valor"] = 396,
	["ability_paladin_artofwar"] = 241,
	["achievement_profession_chefhat"] = 402,
	["inv_misc_ribbon_01"] = 81,
	["inv_misc_gem_variety_01"] = 61,
	["pvpcurrency-conquest-horde"] = 390,
	["pvpcurrency-honor-horde"] = 392,
	["achievement_zone_tolbarad"] = 391,
}
local wlArchaeologyCurrencies = {
	wlCurrencies["trade_archaeology_draenei_artifactfragment"],
	wlCurrencies["trade_archaeology_dwarf_artifactfragment"],
	wlCurrencies["trade_archaeology_fossil_fern"],
	wlCurrencies["trade_archaeology_nerubian_artifactfragment"],
	wlCurrencies["trade_archaeology_highborne_artifactfragment"],
	wlCurrencies["trade_archaeology_orc_artifactfragment"],
	wlCurrencies["trade_archaeology_titan_fragment"],
	wlCurrencies["trade_archaeology_troll_artifactfragment"],
	wlCurrencies["trade_archaeology_vrykul_artifactfragment"],
}

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlNPC, wlOBJECT, wlITEM, wlZONE = 1, 2, 4, 64;

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

--	ID = { spellName, sourceFlags, isLootSpell }
local wlSpells = {
	Disenchanting = { GetSpellInfo(13262) or "", wlITEM, 1 },
	Engineering = { GetSpellInfo(4036) or "", wlNPC, 1 },
	Fishing = { GetSpellInfo(7620) or "", wlZONE, 1 },
	HerbGathering = { GetSpellInfo(2366) or "", wlNPC + wlOBJECT, 1 },
	Milling = { GetSpellInfo(51005) or "", wlITEM, 1 },
	Mining = { GetSpellInfo(2575) or "", wlNPC + wlOBJECT, 1 },
	Opening = { GetSpellInfo(3365) or "", wlOBJECT + wlITEM, 2 },
	PickLocking = { GetSpellInfo(1804) or "", wlOBJECT, 1 },
	PickPocketing = { GetSpellInfo(921) or "", wlNPC, 2 },
	Prospecting = { GetSpellInfo(31252) or "", wlITEM, 1 },
	Skinning = { GetSpellInfo(8613) or "", wlNPC, 1 },
	MindControl = { GetSpellInfo(605) or "", wlNPC, nil },
	BeastLore = { GetSpellInfo(1462) or "", wlNPC, nil },
	Archaeology = { GetSpellInfo(73979) or "", wlOBJECT, 1 },
};

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlFindSpell(name)
	for k,v in pairs(wlSpells) do
		if string.match(name, "^" .. v[1]) then
			return k;
		end
	end

	return nil;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--
local spellCastID = nil;
function wlEvent_UNIT_SPELLCAST_SENT(self, unit, spell, rank, target, lineID)
	if unit ~= "player" or not target or spellCastID then
		return;
	end

	wlClearTracker("spell");

	local spellId = wlFindSpell(spell);

	if spellId then
		
		local npcName, npcUnit = GameTooltip:GetUnit();
		if not npcName and wlUnitName("target") == target then
			npcName, npcUnit = target, "target";
		end

		local itemName, itemLink = GameTooltip:GetItem();

		-- npc
		if bit.band(wlSpells[spellId][2], wlNPC) ~= 0 and npcName and not itemName then

			local name, id = wlUnitName(npcUnit or ""), wlUnitGUID(npcUnit or "");

			if npcName ~= target or name ~= target or not id then
				return;
			end

			wlTracker.spell.kind = "npc";
			wlTracker.spell.id = id;
			wlTracker.spell.name = name;

		-- item
		elseif bit.band(wlSpells[spellId][2], wlITEM) ~= 0 and not npcName and itemName then

			wlTracker.spell.kind = "item";

			if itemName and itemName == target then
				wlTracker.spell.id = wlParseItemLink(itemLink);
				wlTracker.spell.name = itemName;

			elseif target ~= "" then
				wlTracker.spell.id = wlParseItemLink(wlSelectOne(2, GetItemInfo(target)));
				wlTracker.spell.name = target;

			else
				wlTracker.spell.id = nil;
				wlTracker.spell.name = nil;
			end

		-- object
		elseif bit.band(wlSpells[spellId][2], wlOBJECT) ~= 0 and not npcName and not itemName then

			if target == "" then
				return;
			end

			local zone, x, y, dl = wlGetLocation();

			wlRegisterObject(wlConcat(spellId, zone, target));

			wlTracker.spell.kind = "object";
			wlTracker.spell.name = target;
			wlTracker.spell.zone = zone;
			wlTracker.spell.x = x;
			wlTracker.spell.y = y;
			wlTracker.spell.dl = dl;

		-- zone
		elseif bit.band(wlSpells[spellId][2], wlZONE) ~= 0 and not npcName and not itemName then

			if target ~= "" then
				return;
			end

			local zone, x, y, dl = wlGetLocation();

			wlTracker.spell.kind = "zone";
			wlTracker.spell.zone = zone;
			wlTracker.spell.x = x;
			wlTracker.spell.y = y;
			wlTracker.spell.dl = dl;

		else
			return;
		end

		wlTracker.spell.time = wlGetTime();
		wlTracker.spell.event = "SENT";
		wlTracker.spell.action = spellId;
		
		-- associate unit_spellcast_* events
		spellCastID = lineID;
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_UNIT_SPELLCAST_SUCCEEDED(self, unit, spell, rank, lineID, spellId)
	if unit ~= "player" then
		return;
	end

	spellCastID = nil;

	if wlForgeSpells[spellId] then
		wlRegisterObject(WL_FORGE_ID);
	end
	if wlAnvilSpells[spellId] then
		wlRegisterObject(WL_ANVIL_ID);
	end

	if wlTracker.spell.time and wlTracker.spell.event == "SENT" and wlTracker.spell.action == wlFindSpell(spell) then
		wlTracker.spell.event = "SUCCEEDED";
		wlTracker.spell.time = wlGetTime();
	else
		wlClearTracker("spell");
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_UNIT_SPELLCAST_FAILED(self, unit, spell, rank, lineID, spellID)
	-- only reset wlTracker.spell if the 'failed' comes from an associated 'sent'
	if unit == "player" and spellCastID == lineID then
		spellCastID = nil;
		wlClearTracker("spell");
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlUseContainerItem(bag, slot, ...)
	local now = wlGetTime();

	if wlIsValidInterval(wlTracker.spell.time or 0, now, 250) and wlTracker.spell.kind == "item" then
		local id, _, _, _, _, _, _, name = wlParseItemLink(GetContainerItemLink(bag, slot));

		if (wlTracker.spell.id and wlTracker.spell.id ~= id) or (wlTracker.spell.name and wlTracker.spell.name ~= name) then
			wlClearTracker("spell");
			return;
		end

		if not wlTracker.spell.id or not wlTracker.spell.name then
			wlTracker.spell.id = id;
			wlTracker.spell.name = name;
		end

		return;
	end

	if not MerchantFrame:IsShown() and bag and slot then
		local id = wlParseItemLink(GetContainerItemLink(bag, slot));

		wlGameTooltip:ClearLines();
		wlGameTooltip:SetBagItem(bag, slot);

		for i=2,wlGameTooltip:NumLines() do
			local text = getglobal("wlGameTooltipTextLeft" .. i):GetText()
			if text == ITEM_OPENABLE then
				wlClearTracker("spell");

				wlTracker.spell.time = now;
				wlTracker.spell.event = "SUCCEEDED";
				wlTracker.spell.action = "Opening";
				wlTracker.spell.kind = "item";
				wlTracker.spell.id = id;
				wlTracker.spell.name = wlGameTooltipTextLeft1:GetText();
				break;
			end
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_LOOT_OPENED(self)
	if not wlEvent or not wlId or not wlEvent[wlId] or not wlN or not wlEvent[wlId][wlN] then
		return;
	end

	local eventId = wlGetNextEventId();
	local now = wlGetTime();

	-- Clean wlLootCooldown variable
	for k,v in pairs(wlLootCooldown) do
		if v < now - 300000 then -- 5min
			wlLootCooldown[k] = nil;
		end
	end

	if wlTracker.spell.time then
		if (not wlIsValidInterval(wlTracker.spell.time or 0, now, 1000) and not IsFishingLoot()) or wlTracker.spell.event ~= "SUCCEEDED" or not wlSpells[wlTracker.spell.action][3] then
			wlClearTracker("spell");
			return;
		end

		if wlTracker.spell.id then
			wlTracker.spell.name = nil;
		end

		wlTracker.spell.time = nil;
		wlTracker.spell.event = nil;

	elseif wlIsValidName(UnitName("target")) and UnitIsTappedByPlayer("target") and wlUnitIsClose("target") and UnitIsDead("target") and not UnitIsFriend("player", "target") then
		if UnitIsPlayer("target") then
			wlTracker.spell.action = "Killing";
			wlTracker.spell.kind = "player";
			wlTracker.spell.id = wlConcat(wlSelectOne(2, UnitRace("target")), wlSelectOne(2, UnitClass("target")));

		elseif not UnitPlayerControlled("target") then
			wlTracker.spell.action = "Killing";
			wlTracker.spell.kind = "npc";
			wlTracker.spell.id = wlUnitGUID("target");

		else -- pets
			return;
		end

	else
		return;
	--[[
	else -- perhaps Archaeology loot (no real target associate w/ archaeology discovery objects)
		local hasCurrencyLoot = false
		for slot=1,GetNumLootItems() do
			if LootSlotIsCurrency(slot) then
				hasCurrencyLoot = true
				break
			end
		end

		if not hasCurrencyLoot then
			return
		end
	--]]
	end

	-- Loot cooldown
	if wlTracker.spell.action == "Killing" or (wlSpells[wlTracker.spell.action] and wlSpells[wlTracker.spell.action][3] == 1) then
		local guid = nil;

		if wlTracker.spell.kind == "npc" then
			guid = wlConcat(wlTracker.spell.action, UnitGUID("target"));
		elseif wlTracker.spell.kind == "object" then
			guid = wlConcat(wlTracker.spell.action, wlTracker.spell.zone, wlTracker.spell.name);
		end

		if wlLootCooldown[guid] then
			wlClearTracker("spell");
			return;
		end

		if wlIsInParty() then
			SendAddonMessage("WL_LOOT_COOLDOWN", guid, "RAID");
		else
			wlEvent_CHAT_MSG_ADDON(self, "WL_LOOT_COOLDOWN", guid, "RAID", UnitName("player"));
		end
	end

	wlUpdateVariable(wlEvent, wlId, wlN, eventId, "initArray", 0);
	wlEvent[wlId][wlN][eventId].what = "loot";
	wlTableCopy(wlEvent[wlId][wlN][eventId], wlTracker.spell);

	wlEvent[wlId][wlN][eventId].dd = wlGetInstanceDifficulty();

	local flags = 0;

	-- Vitreous Focuser
	if GetItemCount(13370) > 0 then
		flags = flags + 64;
	end

	-- Argent Dawn Trinket
	if IsEquippedItem(12846) or IsEquippedItem(13209) or IsEquippedItem(19812) or IsEquippedItem(23206) or IsEquippedItem(23207) then
		flags = flags + 128;
	end

	-- Alliance or Horde
	local faction = UnitFactionGroup("player");
	if faction == "Alliance" then
		flags = flags + 1024;
	elseif faction == "Horde" then
		flags = flags + 2048;
	end

	wlEvent[wlId][wlN][eventId].flags = flags;

	local tempLoot = {};

	local i = 1;
	for slot=1,GetNumLootItems() do

		if LootSlotIsItem(slot) then
			local itemId = wlParseItemLink(GetLootSlotLink(slot));
			tempLoot[itemId] = (tempLoot[itemId] or 0) + wlSelectOne(3, GetLootSlotInfo(slot));

		elseif LootSlotIsCurrency(slot) then
			wlGameTooltip:ClearLines();
			wlGameTooltip:SetLootCurrency(slot);

			local icon_file_name, currencyName, currencyQuantity, currencyRarity, currencyLocked = GetLootSlotInfo(slot)
			icon_file_name = icon_file_name:match("[^\\]+$"):lower()
			local currencyId = wlCurrencies[icon_file_name]

			wlUpdateVariable(wlEvent, wlId, wlN, eventId, "drop", i, "set", wlConcat("currency", currencyQuantity, currencyId));
			i = i + 1;

		elseif LootSlotIsCoin(slot) then
			wlUpdateVariable(wlEvent, wlId, wlN, eventId, "drop", i, "set", wlConcat("coin", wlParseCoin(select(2, GetLootSlotInfo(slot)))));
			i = i + 1;
		end
	end

	for itemId,qty in pairs(tempLoot) do
		if qty then
			wlUpdateVariable(wlEvent, wlId, wlN, eventId, "drop", i, "set", wlConcat(itemId, qty));
			i = i + 1;
		end
	end

	wlClearTracker("spell");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CHAT_MSG_ADDON(self, id, msg, channel, source)
	if id == "WL_LOOT_COOLDOWN" and msg and msg ~= "" then
		wlLootCooldown[msg] = wlGetTime();
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlIsInParty()
	return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 1;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



----------------------------
----------------------------
--                        --
--   AUCTION  FUNCTIONS   --
--                        --
----------------------------
----------------------------

function wlPlaceAuctionBid(aType, aIndex, bid)
	local id, subId, enchant, socket1, socket2, socket3, socket4 = wlParseItemLink(GetAuctionItemLink(aType, aIndex));
	local count, _, _, _, _, _, buyoutPrice = select(3, GetAuctionItemInfo(aType, aIndex));

	if bid == buyoutPrice and id ~= 0 and enchant == 0 and socket1 == 0 and socket2 == 0 and socket3 == 0 and socket4 == 0 and count > 0 then
		local server = GetRealmName();
		local zone = wlGetLocation();

		wlUpdateVariable(wlAuction, server, zone, wlConcat(id, subId, math.floor(buyoutPrice / count)), "add", 1);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



-------------------------------
-------------------------------
--                           --
--   COMPLETIST  FUNCTIONS   --
--                           --
-------------------------------
-------------------------------

function wlCollect(userInitiated)
	local now = wlGetTime();
	wlUserCollected = userInitiated
	wlMsgCollected = ""

	wlScanQuests(userInitiated)
	wlScanArchaeology(userInitiated)

	if userInitiated and wlScanAllProfessions() then
		wlAppendMsgCollected(WL_COLLECT_PROFESSIONS);
	end

	if wlScanCompanions("MOUNT") then
		wlAppendMsgCollected(WL_COLLECT_MOUNTS);
	end

	if wlScanCompanions("CRITTER") then
		wlAppendMsgCollected(WL_COLLECT_CRITTERS);
	end

	if wlScanCurrencies() then
		wlAppendMsgCollected(WL_COLLECT_CURRENCIES);
	end

	if wlScanGlyphs() then
		wlAppendMsgCollected(WL_COLLECT_GLYPHS);
	end

	if wlScanTitles() then
		wlAppendMsgCollected(WL_COLLECT_TITLES);
	end

	if wlScanSkills() then
		wlAppendMsgCollected(WL_COLLECT_SKILLS);
	else
		wlTimers.skills = now + 5000; -- 5 seconds timeout
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlAppendMsgCollected(value)
	if not wlMsgCollected or wlMsgCollected == "" then
		wlMsgCollected = value;
	elseif wlMsgCollected:match(value) then
	-- do nothing
	elseif not wlMsgCollected:match(WL_COLLECT_LASTSEP) then
		wlMsgCollected = value .. WL_COLLECT_LASTSEP .. wlMsgCollected;
	else
		wlMsgCollected = value .. WL_COLLECT_SEP .. wlMsgCollected;
	end

	wlTimers.printCollected = wlGetTime() + 1000; -- 1 second timeout (wait for other stuff!)
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlTimePlayed_TotalTimeSkip, wlTimePlayed_SkipNext = nil, false;
function wlChatFrame_DisplayTimePlayed(self, totalTime, ...)
	if wlTimePlayed_SkipNext then
		wlTimePlayed_TotalTimeSkip = totalTime;
		wlTimePlayed_SkipNext = false;

	elseif wlTimePlayed_TotalTimeSkip ~= totalTime then
		wlDefaultChatFrame_DisplayTimePlayed(self, totalTime, ...);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetAddedCurrency(delta)
	for k,v in pairs(delta.changed) do
		if v > 0 then return k, v end
	end
	for k,v in pairs(delta.added) do
		if v > 0 then return k, v end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_CURRENCY_DISPLAY_UPDATE(...)
-- generate current list of owned currencies
	local _, delta = wlScanCurrencies()

	-- see if a boss awarded some currency
	-- don't clean up wlMostRecentEliteKilled here - this may get called several times w/ varying currency awards
	local now = wlGetTime()
	if wlMostRecentEliteKilled and wlMostRecentEliteKilled.id and wlMostRecentEliteKilled.timeOfDeath then
		if now <= (wlMostRecentEliteKilled.timeOfDeath + 1000) then -- 1 seconds to get currency reward from last elite killed
			local currencyId, currencyAmount = wlGetAddedCurrency(delta)

			-- make sure we have something to report
			if currencyId and currencyAmount then
				local eventId = wlGetNextEventId()
				local eliteId = wlMostRecentEliteKilled.id

				wlUpdateVariable(wlEvent, wlId, wlN, eventId, "initArray", 0)
				wlEvent[wlId][wlN][eventId].action = "Killing"
				wlEvent[wlId][wlN][eventId].kind = "npc"
				wlEvent[wlId][wlN][eventId].what = "award"
				wlEvent[wlId][wlN][eventId].id = eliteId
				wlEvent[wlId][wlN][eventId].dd = wlGetInstanceDifficulty();

				-- Alliance or Horde
				local flags = 0
				local faction = UnitFactionGroup("player");
				if faction == "Alliance" then
					flags = flags + 1024;
				elseif faction == "Horde" then
					flags = flags + 2048;
				end

				wlEvent[wlId][wlN][eventId].flags = flags;

				-- Add what kind of currency and how much there was
				wlUpdateVariable(wlEvent, wlId, wlN, eventId, "drop", 1, "set", "currency^" .. currencyAmount .. "^" .. currencyId)
			end
		end
	end

	-- update our list of archaeology fragments
	wlScanArchaeology()
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanCurrencies()
	local oldScannedCurrencies = wlScans.currencies
	if not oldScannedCurrencies or "table" ~= type(oldScannedCurrencies) then
		oldScannedCurrencies = {}
	end

	local scannedCurrencies = {}
	local foundACurrency = false

	-- collect current currency information
	for k,currencyIndex in pairs(wlCurrencies) do
		local localized_label, amount, icon_file_name = GetCurrencyInfo(currencyIndex)
		icon_file_name = string.lower(icon_file_name)
		if localized_label and "" ~= localized_label and k == icon_file_name and amount > 0 then
			scannedCurrencies[currencyIndex] = amount
			foundACurrency = true
		end
	end

	-- build a profile currency string
	if foundACurrency then
		wlScans.currencies = scannedCurrencies
	else
		wlScans.currencies = "-1"
	end

	-- generate the list of currency changes
	-- .changed => { currencyId = delta }
	-- .missing => { currencyId = 0 }
	-- .added => { currencyId = amount }
	-- first 'changed' and 'missing'
	local delta = {
		changed = {},
		missing = {},
		added = {}
	}
	for k,v in pairs(scannedCurrencies) do
		if oldScannedCurrencies[k] then -- possibly changed
			delta.changed[k] = v - oldScannedCurrencies[k]
		else -- added
			delta.added[k] = v
		end
		oldScannedCurrencies[k] = nil
	end
	-- missing
	for k,v in pairs(oldScannedCurrencies) do
		delta.missing[k] = 0
	end

	return foundACurrency, delta
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlFlattenArchaeology()
	if "string" == type(wlScans.archaeology) then
		return wlScans.archaeology -- should be "-1", a.k.a. 'seen it, no data to report'
	elseif "table" == type(wlScans.archaeology) then

		-- [<projectSpellId>,<projectSpellId>,...;<projectCompletionCount>,<projectCompletionCount>,...]

		local spellIds = {}
		local completionCounts = {}

		for raceName, raceData in pairs(wlScans.archaeology) do
			-- skip this race if there was an error
			if raceData.current and not raceData.error and raceData.completed and not raceData.completed.error then
				for spellId, completionCount in pairs(raceData.completed) do
					table.insert(spellIds, spellId)
					table.insert(completionCounts, completionCount)
				end
			end
		end

		if #spellIds ~= #completionCounts then
			return "" -- these must be equal, if not, claim not to have seen any data
		end

		spellIds = table.concat(spellIds, ",")
		completionCounts = table.concat(completionCounts, ",")
		if #spellIds > 0 and #completionCounts > 0 then
			return spellIds .. ":" .. completionCounts
		else
			return "" -- there was an error, claim not to have seen any data
		end
	else
		return "" -- a.k.a. 'did not see any data'
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanArtifactsAddError(completed, artifactName, message)
	if not completed or not message then
		return
	end

	if not completed.error then
		completed.error = {}
	end

	if not artifactName then
		artifactName = "<unknownArtifactName>"
	end

	local finalMessage = message:gsub("~ID~",artifactName);
	table.insert(completed.error, finalMessage)
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlWaitingCompletionHistory = false
function wlScanArtifacts()
	local archaeology = {}
	local foundAnArtifact = false
	local numRaces = GetNumArchaeologyRaces()

	if numRaces > 0 then
		for raceIndex=1,numRaces do
		-- name, currency, texture, itemID =  GetArchaeologyRaceInfo(i)
			local raceInfo = { GetArchaeologyRaceInfo(raceIndex) }
			local raceName = raceInfo[1]

			local numArtifacts = GetNumArtifactsByRace(raceIndex)
			if numArtifacts > 0 then
				archaeology[raceName] = { ["index"] = raceIndex }

				-- name, history, rarity, iconPath, artifactDescription, socketCount, bgRaceLogoTexture
				local currentProject = { GetActiveArtifactByRace(raceIndex) }

				-- current project
				if currentProject[4] then
					local currentIconName = currentProject[4]:match("[^\\]+$"):lower()
					if currentIconName and wlArtifacts[currentIconName] then
						archaeology[raceName].current = wlArtifacts[currentIconName].spell
					elseif not wlArtifacts[currentIconName] then
						wlScanArtifactsAddError(
							archaeology[raceName],
							currentProject[1],
							"ERROR: missing entry in Archaeology.lua for '~ID~'")
					else
						wlScanArtifactsAddError(
							archaeology[raceName],
							currentProject[1],
							"ERROR: missing (or unexpected path format) iconPath for artifact '~ID~'.")
					end
				end

				-- completed projects
				archaeology[raceName].completed = {}
				for artifactIndex=1,numArtifacts do
				-- name, description, rarity, iconPath, spellDescription,  _, _, firstCompletionTime, completionCount
					local artifactInfo = { GetArtifactInfoByRace(raceIndex, artifactIndex) }

					if artifactInfo[4] and artifactInfo[9] and artifactInfo[9] > 0 then
						local iconName = artifactInfo[4]:match("[^\\]+$"):lower()
						if iconName and wlArtifacts[iconName] then
							local completedSpellId = wlArtifacts[iconName].spell
							if completedSpellId then
							-- record the completion count
								archaeology[raceName].completed[completedSpellId] = artifactInfo[9]
							else
							-- don't know how to map that iconName to a spellId - maybe a new/changed iconPath for that artifact?
								wlScanArtifactsAddError(
									archaeology[raceName].completed,
									artifactInfo[1],
									"ERROR: missing spell entry in Archaeology.lua for '~ID~'")
							end

							foundAnArtifact = true
						else
						-- unexpected iconName failure or missing artifact entry in Archaeology.lua
							if not iconName then
								wlScanArtifactsAddError(
									archaeology[raceName].completed,
									artifactInfo[1],
									"ERROR: unexpected path format in iconPath for '~ID~'.  The iconPath was: " .. artifactInfo[4])
							elseif not wlArtifacts[iconName] then
								wlScanArtifactsAddError(
									archaeology[raceName].completed,
									artifactInfo[1],
									"ERROR: missing entry in Archaeology.lua for '~ID~'")
							end
						end
					elseif not artifactInfo[4] then
					-- missing iconPath information for this artifact
						wlScanArtifactsAddError(
							archaeology[raceName].completed,
							artifactInfo[1],
							"ERROR: missing iconPath for '~ID~'")
					end
				end
			end
		end
	end

	wlTimers.archaeology = nil -- clear the timer
	wlWaitingCompletionHistory = false

	wlAppendMsgCollected(WL_COLLECT_ARCHAEOLOGY);

	if foundAnArtifact then
		wlScans.archaeology = archaeology
		return archaeology
	else
		wlScans.archaeology = "-1" -- WARNING - it may be a string now!
	end

end

function wlEvent_ARTIFACT_UPDATE(...)
	wlScanArtifacts()
end

function wlEvent_ARTIFACT_COMPLETE(...)
	wlScanArtifacts()
end

function wlEvent_ARTIFACT_HISTORY_READY(...)
	wlScanArtifacts()
end

function wlScanArchaeology()
	local _, _, arch = GetProfessions()
	if arch then
		if not IsArtifactCompletionHistoryAvailable() then
		-- request missing archaeology completion history
			RequestArtifactCompletionHistory()
			wlWaitingCompletionHistory = true
			wlTimers.archaeology = wlGetTime() + 10000 -- check again in 10 seconds
		else
		-- only do the work in the event response to the request . . .
			wlScanArtifacts()
		end
	else
	-- successful scan with nothing to report
		wlScans.archaeology = "-1" -- WARNING - it's a string now!
		wlAppendMsgCollected(WL_COLLECT_ARCHAEOLOGY)
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanQuests(userInitiated)
	QueryQuestsCompleted()
	wlTimers.quests = wlGetTime() + 10000 -- 10 second timeout
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlQueryTimePlayed()
-- Don't display time played in chat frame if not queried by player
	wlTimePlayed_SkipNext = true;

	RequestTimePlayed();
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlProfessionSpells = {
	2259, -- Alchemy
	2018, -- Blacksmith
	7411, -- Enchanting
	4036, -- Engineering
	45357, -- Inscription
	25229, -- Jewelcrafting
	2108, -- Leatherworking
	2656, -- Smelting (Mining)
	3908, -- Tailoring
	2550, -- Cooking
	3273 -- First Aid
};
local wlProfessionSkillNameToSpellName = {
	['Mining'] = 'Smelting'
}

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local seenProfessions = {}

function wlListMissingProfessions()
-- Make sure we've seen all the known professions at least once before now
	local missingProfessions = {}
	for spellId,spellName in pairs(wlScans.profs) do
		if not seenProfessions[spellName] then
			table.insert(missingProfessions, spellName)
		end
	end
	if #missingProfessions > 0 then
		local missing = table.concat(missingProfessions, ', ')
		return missing
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlAllProfessionsSeen()
	for _,spellId in iPairs(wlProfessionSpells) do
		local spellName = GetSpellInfo(spellId)
		if not seenProfessions[spellName] then return false end
	end

	return true
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanKnownProfessions()
	wlScans.profs = {}

	for _,spellId in ipairs(wlProfessionSpells) do
		local spellName = GetSpellInfo(spellId)
		if IsUsableSpell(spellName) then
			wlScans.profs[spellId] = spellName
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlSpammedNeededRecipesOnce = false
function wlEvent_TRADE_SKILL_SHOW(self, ...)
	local skillLineName = GetTradeSkillLine()

	if skillLineName then
		wlScanKnownProfessions()

		-- grumble, grumble, grumble
		skillLineName = wlProfessionSkillNameToSpellName[skillLineName] or skillLineName

		if not seenProfessions[skillLineName] then
		-- Make the user aware of any other skills that need to be seen
			seenProfessions[skillLineName] = true
			local missing = wlListMissingProfessions()
			if missing then
				if not wlSpammedNeededRecipesOnce then
					wlSpammedNeededRecipesOnce = true
					wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_COLLECT_UNSN_TIP))
					wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_COLLECT_UNSN_ALL_MSG:format(missing)))
				end
			else
			-- maybe print a "Recipe list(s) ready for '/wl collect'." here?
			end
		end
	end

	-- it's okay to run this now even if we can't guarantee all the spells are available at this time
	wlScanProfessionWindow(wlGrabTradeSkillTools)
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--
-- This group of functions each handle a single spell entry in the trade skill window
--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- a trade skill spell handler
function wlGrabTradeSkillTools(skillLineName, spellId, tradeSkillIndex)
	-- Process Anvil and Forge detection
	local tools = GetTradeSkillTools(tradeSkillIndex);

	if tools then
		if string.find(tools, "Anvil") ~= nil then
			wlAnvilSpells[tonumber(spellId)] = true;
		end
		if string.find(tools, "Forge") ~= nil then
			wlForgeSpells[tonumber(spellId)] = true;
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- a trade skill spell handler
function wlGrabTradeSkillRecipe(skillLineName, spellId, tradeSkillIndex)
	table.insert(wlScans.recipes[skillLineName], spellId);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--
-- roll through the trade skill window, doing something interesting with each spell found
--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--
function wlScanProfessionWindow(...)
	-- do some sanity checking
	local parameterCount = select('#',...)
	if parameterCount <= 0 then return end
	for funcIndex = 1, parameterCount do
		if 'function' ~= type(select(funcIndex,...)) then return end
	end

	local skillLineName = GetTradeSkillLine();
	skillLineName = wlProfessionSkillNameToSpellName[skillLineName] or skillLineName

	-- plow through the display list

	local wlCollapsedHeaders = {};
	TradeSkillFrame:UnregisterEvent("TRADE_SKILL_UPDATE");

	-- Clear all filters
	TradeSkillOnlyShowMakeable(false);
	TradeSkillOnlyShowSkillUps(false);

	local invSlot = 0;
	for i=0,select("#", GetTradeSkillInvSlots()) - 1 do
		if GetTradeSkillInvSlotFilter(i) then
			invSlot = i;
			break;
		end
	end

	local subClass = 0;
	for i=0,select("#", GetTradeSkillSubClasses()) - 1 do
		if GetTradeSkillSubClassFilter(i) then
			subClass = i;
			break;
		end
	end

	TradeSkillOnlyShowMakeable(false);
	SetTradeSkillSubClassFilter(0, 1, 1);
	SetTradeSkillInvSlotFilter(0, 1, 1);

	for tradeSkillIndex = 1,GetNumTradeSkills() do
		local name, category, _, isExpanded = GetTradeSkillInfo(tradeSkillIndex);
		if name then
			if category == "header" then
				if not isExpanded then
					ExpandTradeSkillSubClass(tradeSkillIndex);
					wlCollapsedHeaders[name] = 1;
				end
			else
				local _, _, spellId = string.find(GetTradeSkillRecipeLink(tradeSkillIndex), "^|%x+|Henchant:(.+)|h%[.+%]");

				-- run the handlers for this trade skill spell
				for funcIndex = 1, parameterCount do
					select(funcIndex,...)(skillLineName, spellId, tradeSkillIndex)
				end
			end
		end
	end

	-- Restore headers
	for i=1,GetNumTradeSkills() do
		local name, category, _, isExpanded = GetTradeSkillInfo(i);

		if name and category == "header" and wlCollapsedHeaders[name] then
			CollapseTradeSkillSubClass(i);
		end
	end

	-- Restore filters
	TradeSkillOnlyShowMakeable(TradeSkillFrame.filterTbl.hasMaterials);
	TradeSkillOnlyShowSkillUps(TradeSkillFrame.filterTbl.hasSkillUp);
	SetTradeSkillSubClassFilter(subClass, 1, 1);
	SetTradeSkillInvSlotFilter(invSlot, 1, 1);

	TradeSkillFrame:RegisterEvent("TRADE_SKILL_UPDATE");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanProfession()
	local skillLineName = GetTradeSkillLine();
	skillLineName = wlProfessionSkillNameToSpellName[skillLineName] or skillLineName

	if not wlIsValidName(skillLineName) or IsTradeSkillLinked() or IsTradeSkillGuild() then
		return;
	end

	if not seenProfessions[skillLineName] then return end

	wlScans.recipes[skillLineName] = {};

	wlScanProfessionWindow(wlGrabTradeSkillRecipe)

	return skillLineName;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local collectReminded = false
function wlScanAllProfessions()
	if not IsAddOnLoaded("Blizzard_TradeSkillUI") then
		LoadAddOn("Blizzard_TradeSkillUI");
	end

	wlScanKnownProfessions()

	-- Make sure we've seen all the known professions at least once before now
	local missing = wlListMissingProfessions()
	if missing then
		if not wlSpammedNeededRecipesOnce then
			wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_COLLECT_UNSN_TIP), true, 'wlScannProfessionTIP', 15000)
			wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_COLLECT_UNSN_ALL_MSG:format(missing)), true, 'wlScanProfessions')
		end
		wlSpammedNeededRecipesOnce = true
		return false
	end

	-- Disable sounds
	local sound = GetCVar("Sound_EnableSFX");
	SetCVar("Sound_EnableSFX", 0);

	CloseTradeSkill();

	wlFrame:UnregisterEvent("TRADE_SKILL_SHOW");
	TradeSkillFrame:UnregisterEvent("TRADE_SKILL_SHOW");
	TradeSkillFrame:UnregisterEvent("TRADE_SKILL_CLOSE");

	-- wipe existing recipe list, and make sure it's no longer ""
	wlScans.recipes = {}

	local skillLineName = ""
	local skillFailed = false
	for _,spellId in ipairs(wlProfessionSpells) do
		local spellName = GetSpellInfo(spellId);
		if wlScans.profs[spellId] then
			CastSpellByName(spellName);
			skillLineName = wlScanProfession()
			skillFailed = not skillLineName
			if skillFailed then
				wlScans.recipes = ""
				break
			end
		end
	end

	-- If the scan was successful but we found no recipes, tell the server "-1" (update data by: delete existing, add nothing)
	if 0 == wlTableCountValues(wlScans.recipes) and not skillFailed then wlScans.recipes = "-1" end

	CloseTradeSkill();

	TradeSkillFrame:RegisterEvent("TRADE_SKILL_CLOSE");
	TradeSkillFrame:RegisterEvent("TRADE_SKILL_SHOW");
	wlFrame:RegisterEvent("TRADE_SKILL_SHOW");

	-- Restore sound setting
	SetCVar("Sound_EnableSFX", sound);

	-- Remind the user to upload the data
	if not collectReminded then
		collectReminded = true;
		wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_COLLECT_TIP));
	end

	return not skillFailed;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanCompanions(kind) -- "CRITTER" or "MOUNT"
	local ids = "";

	for i=1,GetNumCompanions(kind) do
		if i > 1 then
			ids = ids .. ",";
		end

		ids = ids .. select(3, GetCompanionInfo(kind, i));
	end

	if ids ~= "" then
		wlScans.pets[kind] = ids;
		return true;
	else
	-- Tell the server we scanned the Companions/Mounts, and found none (Update this field by: Delete All, Add Nothing)
		wlScans.pets[kind] = "-1";
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanGlyphs()
	if not IsAddOnLoaded("Blizzard_GlyphUI") then
		LoadAddOn("Blizzard_GlyphUI");
	end

	local ids = "";

	local filterText = GlyphFrameSearchBox:GetText();
	SetGlyphNameFilter("");

	local filterKnown, filterUnknown = IsGlyphFlagSet(GLYPH_FILTER_KNOWN), IsGlyphFlagSet(GLYPH_FILTER_UNKNOWN);

	--	That function is bugged, so I use ToggleGlyphFilter instead... +o(
	--	SetGlyphFilter(GLYPH_FILTER_KNOWN, true);
	--	SetGlyphFilter(GLYPH_FILTER_UNKNOWN, true);

	if not filterKnown then
		ToggleGlyphFilter(GLYPH_FILTER_KNOWN); -- Show
	end

	if filterKnown then
		ToggleGlyphFilter(GLYPH_FILTER_UNKNOWN); -- Hide
	end

	local toggleFilter1, toggleFilter2, toggleFilter4 = not IsGlyphFlagSet(1), not IsGlyphFlagSet(2), not IsGlyphFlagSet(4);
	ToggleGlyphFilter((toggleFilter1 and 1 or 0) + (toggleFilter2 and 2 or 0) + (toggleFilter4 and 4 or 0));

	local i = 1;
	while i <= GetNumGlyphs() do
		local name, _, isKnown, _, castSpell = GetGlyphInfo(i);

		if name ~= "header" and isKnown then
			if ids:len() ~= 0 then
				ids = ids .. ",";
			end

			ids = ids .. castSpell;
		end

		i = i + 1;
	end

	ToggleGlyphFilter((toggleFilter1 and 1 or 0) + (toggleFilter2 and 2 or 0) + (toggleFilter4 and 4 or 0));

	--	That function is bugged, so I use ToggleGlyphFilter instead... +o(
	--	SetGlyphFilter(GLYPH_FILTER_KNOWN, true);
	--	SetGlyphFilter(GLYPH_FILTER_UNKNOWN, true);

	if not filterKnown then
		ToggleGlyphFilter(GLYPH_FILTER_KNOWN); -- Hide
	end

	if filterKnown then
		ToggleGlyphFilter(GLYPH_FILTER_UNKNOWN); -- Show
	end

	if filterText ~= SEARCH then
		SetGlyphNameFilter(filterText);
	end

	wlPrint(ids);

	if ids ~= "" then
		wlScans.glyphs = ids;
		return true;
	else
	-- Tell the server we scanned the glyphs, and found none (Update this field by: Delete All, Add Nothing)
		wlScans.glyphs = "-1";
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanSkills()
	local ids, first = "", true;

	local professions = { GetProfessions() };

	for _,index in pairs(professions) do
		local _, _, rank, maxRank, _, _, skillLine = GetProfessionInfo(index);

		if first then
			first = false;
		else
			ids = ids .. ",";
		end

		ids = ids .. skillLine .. "," .. rank .. "," .. maxRank;
	end

	if ids ~= "" then
		wlScans.skills = ids;
		return true;
	else
	-- A failure here doesn't mean there are no skills, just that we haven't
	-- received a response from the server yet.  We'll keep trying
	-- until we get some data.  Don't set this to "-1" (wipe,no add)
		wlScans.skills = "";
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlScanTitles(userInitiated)
	local ids, first = "", true;

	for i=1,GetNumTitles() do
		if IsTitleKnown(i) ~= 0 then
			if first then
				first = false;
			else
				ids = ids .. ",";
			end

			ids = ids .. i;
		end
	end

	if ids ~= "" then
		wlScans.titles = ids;
		return true;
	else
	-- Tell the server we scanned the titles, and found none (Update this field by: Delete All, Add Nothing)
		wlScans.titles = "-1";
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetExportDataValue()
	local timePlayed = 0;

	if wlScans.timePlayedTotal and wlScans.timePlayedWhen then
	-- Correct timePlayedTotal by adding amount of seconds since last update
		timePlayed = wlScans.timePlayedTotal + math.floor((wlGetTime() - wlScans.timePlayedWhen) / 1000);
	end

	local quests = type(wlScans.quests) == "table" and wlTableConcatKeys(wlScans.quests, ",") or wlScans.quests;
	local recipes = type(wlScans.recipes) == "table" and wlTableConcatValues(wlScans.recipes, ",") or wlScans.recipes;
	local value = "&quests=" .. quests;
	value = value .. "&profs=" .. wlTableConcatKeys(wlScans.profs, ",");
	value = value .. "&recipes=" .. recipes;
	value = value .. "&mounts=" .. wlScans.pets.MOUNT;
	value = value .. "&companions=" .. wlScans.pets.CRITTER;
	value = value .. "&glyphs=" .. wlScans.glyphs;
	value = value .. "&skills=" .. wlScans.skills;
	value = value .. "&titles=" .. wlScans.titles;
	value = value .. "&currencies=" .. (("string" == type(wlScans.currencies) and wlScans.currencies) or wlTableConcatKeyValues(wlScans.currencies, ","));
	value = value .. "&projects=" .. wlFlattenArchaeology()
	value = value .. "&timePlayedTotal=" .. timePlayed;

	return value
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRefreshExportData()
	wlUserCollected = false;
	--wlScanCurrencies();
	--wlScanArchaeology();
	--wlScanCompanions("MOUNT");
	--wlScanCompanions("CRITTER");
	--wlScanGlyphs();
	--wlScanSkills();
	-- Players reporting this not working during PLAYER_LOGOUT event
	--wlScanTitles();

	local key = "who=" .. wlConcatToken(",", GetCVar("realmList"), GetRealmName(), wlSelectOne(1, UnitName("player")), GetAccountExpansionLevel());
	local value = wlGetExportDataValue()

	-- Only add the data to the wlExportData if the character is level 10 or above
	-- Data is still collected, just not uploaded until level 10
	if UnitLevel("player") > 10 then
		if wlExportData:find(key) then
			wlExportData = wlExportData:gsub(key .. "[^;]*", key .. value); -- replace
		else
			wlExportData = wlExportData .. key .. value .. ";";
		end
	end
end



--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--
--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--
--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetLocation()
	local zone = GetRealZoneText() or "";
	local x, y = GetPlayerMapPosition("player");
	local dl = GetCurrentMapDungeonLevel() or 0;

	if not x or not y then
		x, y = 0, 0;
	end

	if x == 0 and y == 0 then
		for level=1,GetNumDungeonMapLevels() do
			SetDungeonMapLevel(level);
			x, y = GetPlayerMapPosition("player");

			if x and y and (x > 0 or y > 0) then
				SetDungeonMapLevel(dl); -- Restore
				dl = level;
				break;
			end

			x, y = 0, 0;
		end
	end

	if DungeonUsesTerrainMap() then
		dl = dl - 1;
	end

	return zone, math.floor(x * 1000 + 0.5), math.floor(y * 1000 + 0.5), dl;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetLocationIndex(array, x, y, dl, delta)
	for i=1,array.n do
		if wlIsEqualValues(x, array[i].x, delta) and wlIsEqualValues(y, array[i].y, delta) and array[i].dl == dl then
			return i;
		end
	end

	return nil;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRound(n, p)
	local p = math.pow(10, p);
	return math.floor(n * p + 0.5) / p;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlClearTracker(...)
	for i=1,select("#", ...) do
		local n = wlSelectOne(i, ...);
		wlUpdateVariable(wlTracker, n, "time", "set", nil);

		for k,_ in pairs(wlTracker[n]) do
			wlTracker[n][k] = nil;
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



--------------------------
--------------------------
--                      --
--   FRAME  FUNCTIONS   --
--                      --
--------------------------
--------------------------

local wlEvents = {
	-- player
	PLAYER_LOGIN = wlEvent_PLAYER_LOGIN,
	PLAYER_LOGOUT = wlEvent_PLAYER_LOGOUT,
	ADDON_LOADED = wlEvent_ADDON_LOADED,

	-- npc
	PLAYER_TARGET_CHANGED = wlEvent_PLAYER_TARGET_CHANGED,
	CHAT_MSG_MONSTER_SAY = wlEvent_CHAT_MSG_MONSTER_SAY,
	CHAT_MSG_MONSTER_WHISPER = wlEvent_CHAT_MSG_MONSTER_WHISPER,
	CHAT_MSG_MONSTER_YELL = wlEvent_CHAT_MSG_MONSTER_YELL,
	GOSSIP_SHOW = wlEvent_GOSSIP_SHOW,
	AUCTION_HOUSE_SHOW = wlEvent_AUCTION_HOUSE_SHOW,
	BANKFRAME_OPENED = wlEvent_BANKFRAME_OPENED,
	BATTLEFIELDS_SHOW = wlEvent_BATTLEFIELDS_SHOW,
	CONFIRM_BINDER = wlEvent_CONFIRM_BINDER,
	CONFIRM_PET_UNLEARN = wlEvent_CONFIRM_PET_UNLEARN,
	CONFIRM_TALENT_WIPE = wlEvent_CONFIRM_TALENT_WIPE,
	GOSSIP_ENTER_CODE = wlEvent_GOSSIP_ENTER_CODE,
	OPEN_TABARD_FRAME = wlEvent_OPEN_TABARD_FRAME,
	PET_STABLE_SHOW = wlEvent_PET_STABLE_SHOW,
	TAXIMAP_OPENED = wlEvent_TAXIMAP_OPENED,
	PET_BAR_UPDATE = wlEvent_PET_BAR_UPDATE,
	MERCHANT_SHOW = wlEvent_MERCHANT_SHOW,
	MERCHANT_UPDATE = wlEvent_MERCHANT_UPDATE,
	TRAINER_SHOW = wlEvent_TRAINER_SHOW,
	CHAT_MSG_MONSTER_EMOTE = wlEvent_CHAT_MSG_MONSTER_EMOTE,
	COMBAT_LOG_EVENT_UNFILTERED = wlEvent_COMBAT_LOG_EVENT_UNFILTERED,
	UPDATE_FACTION = wlEvent_UPDATE_FACTION,

	-- drops
	LOOT_OPENED = wlEvent_LOOT_OPENED,
	CHAT_MSG_ADDON = wlEvent_CHAT_MSG_ADDON,
	UNIT_SPELLCAST_SENT = wlEvent_UNIT_SPELLCAST_SENT,
	UNIT_SPELLCAST_SUCCEEDED = wlEvent_UNIT_SPELLCAST_SUCCEEDED,
	UNIT_SPELLCAST_FAILED = wlEvent_UNIT_SPELLCAST_FAILED,
	UNIT_SPELLCAST_INTERRUPTED = wlEvent_UNIT_SPELLCAST_FAILED,
	UNIT_SPELLCAST_FAILED_QUIET = wlEvent_UNIT_SPELLCAST_FAILED,

	-- object
	ITEM_TEXT_BEGIN = wlEvent_ITEM_TEXT_BEGIN,
	MAIL_SHOW = wlEvent_MAIL_SHOW,

	-- quest
	QUEST_DETAIL = wlEvent_QUEST_DETAIL,
	QUEST_PROGRESS = wlEvent_QUEST_PROGRESS,
	QUEST_COMPLETE = wlEvent_QUEST_COMPLETE,
	QUEST_LOG_UPDATE = wlEvent_QUEST_LOG_UPDATE,
	COMBAT_TEXT_UPDATE = wlEvent_COMBAT_TEXT_UPDATE,
	CHAT_MSG_COMBAT_FACTION_CHANGE = wlEvent_CHAT_MSG_COMBAT_FACTION_CHANGE,
	CHAT_MSG_COMBAT_HONOR_GAIN = wlEvent_CHAT_MSG_COMBAT_HONOR_GAIN,
	QUEST_QUERY_COMPLETE = wlEvent_QUEST_QUERY_COMPLETE,

	-- coords tooltip
	PLAYER_ENTERING_WORLD = wlEvent_ZONE_CHANGED,
	ZONE_CHANGED = wlEvent_ZONE_CHANGED,
	ZONE_CHANGED_NEW_AREA = wlEvent_ZONE_CHANGED,

	-- completist
	TIME_PLAYED_MSG = wlEvent_TIME_PLAYED_MSG,
	TRADE_SKILL_SHOW = wlEvent_TRADE_SKILL_SHOW,
	CURRENCY_DISPLAY_UPDATE = wlEvent_CURRENCY_DISPLAY_UPDATE,
	ARTIFACT_HISTORY_READY = wlEvent_ARTIFACT_HISTORY_READY,
	ARTIFACT_UPDATE = wlEvent_ARTIFACT_UPDATE,
	ARTIFACT_COMPLETE = wlEvent_ARTIFACT_COMPLETE,
};

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- Get a numeric client version number
function wlGetClientVersion()
	local version = GetBuildInfo()
	local major,minor,patch = strsplit(".", version)
	return ((10000*major) + (100*minor) + (patch))
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- Support PTR version
wlClientVersion = wlGetClientVersion()
if wlClientVersion >= 40200 then
	wlEvents.COMBAT_LOG_EVENT_UNFILTERED = wlEvent_COMBAT_LOG_EVENT_UNFILTERED_40200
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wl_OnLoad(self)
	CreateFrame("Frame", "wlUpdateFrame", UIParent);
	wlUpdateFrame:SetScript('OnUpdate', wl_OnUpdate);
	wlUpdateFrame:Show();

	for event,_ in pairs(wlEvents) do
		self:RegisterEvent(event);
	end

	SlashCmdList["WOWHEAD_LOOTER"] = wlParseCommand;
	SLASH_WOWHEAD_LOOTER1 = "/wl";

	table.insert(UISpecialFrames, "wlDebugFrame");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wl_OnEvent(self, event, ...)
	if event and wlEvents[event] then
		wlEvents[event](self, ...);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local uploadReminder = false;
function wl_OnUpdate(self)
	local now = wlGetTime();

	for name,timeout in pairs(wlTimers) do
		if timeout and now - timeout >= 0 then
			wlTimers[name] = false; -- Clear timer

			if name == "quests" then
				QueryQuestsCompleted();
				wlTimers.quests = now + 10000; -- 10 seconds timeout

			elseif name == "archaeology " then
				RequestArtifactCompletionHistory();
				wlTimers.archaeology = now + 10000; -- 10 seconds timeout

			elseif name == "skills" then
				if wlScanSkills() then
					wlAppendMsgCollected(WL_COLLECT_SKILLS);
				else
					wlTimers.skills = now + 5000; -- 5 seconds timeout
				end

			elseif name == "autoCollect" then
				wlQueryTimePlayed();
				wlCollect();

			elseif name == "printCollected" then
				if wlMsgCollected and wlMsgCollected ~= "" then
					wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_COLLECT_MSG:format(wlMsgCollected)), wlUserCollected);

					if not uploadReminder then
						uploadReminder = true;
						wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_UPLOAD_REMINDER), wlUserCollected);
					end

					wlMsgCollected = nil;
					wlUserCollected = false;
				end
			end
		end
	end
	
	-- clean up last elite death - for tracking currencies awarded from bosses
	if wlMostRecentEliteKilled and wlMostRecentEliteKilled.timeOfDeath and now > (wlMostRecentEliteKilled.timeOfDeath + 1000) then
		wlMostRecentEliteKilled = {}
	end

	-- filter out unwanted NPC location collection
	if not UnitExists("target") or UnitPlayerControlled("target") or UnitIsTapped("target") or UnitAffectingCombat("target") or not wlUnitIsClose("target") then
		return;
	end
	
	local id, kind = wlUnitGUID("target");

	if id and kind == "npc" then
		wlRegisterUnitLocation(id, UnitLevel("target"));
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlParseCommand(cmd)
	local param1, param2, param3 = cmd:lower():match("^([%S]+)%s*([%S]*)%s*(.*)$");
	param1, param2 = param1 or "", param2 or "";

	if param1 == "debug" then

		if param2 == "show" then
			wlDebugFrame:Show();
		elseif param2 == "hide" then
			wlDebugFrame:Hide();
		elseif param2 == "clear" then
			wlDebugEdit:SetText("");
		elseif param2 == "toggle" then
			if wlDebugFrame:IsShown() then
				wlDebugFrame:Hide();
			else
				wlDebugFrame:Show();
			end
		elseif param2 == "reset" then
			wlReset();
			ReloadUI();
		elseif param2 == "test" then
			wlTest(cmd);
		end

	elseif param1 == "loc" then

		if param2 == "map" then
			wlFrameToggle(wlLocMapFrame, WL_LOC_MAP, "locMap");
		elseif param2 == "tooltip" then
			wlFrameToggle(wlLocTooltipFrame, WL_LOC_TOOLTIP, "locTooltip");
		elseif param2 == "reset" then
			wlFrameReset(wlLocTooltipFrame);
		else
			local _, x, y = wlGetLocation();
			wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, WL_LOC:format(x / 10, y / 10)), true);
		end

	elseif param1 == "id" then

		if param2 == "reset" then
			wlFrameReset(wlIdTooltipFrame);
		else
			wlFrameToggle(wlIdTooltipFrame, WL_ID_TOOLTIP, "idTooltip");
		end

	elseif param1:match("^collect$") then
		wlTimers["autoCollect"] = nil; -- Cancel auto-collect
		collectReminded = true; -- Don't tip the user about the command s/he just used
		wlCollect(true);

	elseif param1:match("^recipes?$") then
		local skillLineName = GetTradeSkillLine();
		if wlIsValidName(skillLineName) and not IsTradeSkillLinked() and not IsTradeSkillGuild() and "-1" ~= wlScans.recipes and wlScans.recipes[skillLineName] then
			wlPopupBox(string.format(WL_LIST_RECIPES, skillLineName), table.concat(wlScans.recipes[skillLineName], ","));
		end

	elseif param1:match("^mounts?$") then
		wlUserCollected = false; -- Don't spam the user twice
		wlScanCompanions("MOUNT");
		wlPopupBox(WL_LIST_MOUNTS, wlScans.pets.MOUNT);

	elseif param1:match("^companions?$") then
		wlUserCollected = false; -- Don't spam the user twice
		wlScanCompanions("CRITTER");
		wlPopupBox(WL_LIST_COMPANIONS, wlScans.pets.CRITTER);

	elseif param1:match("^glyphs?$") then
		wlUserCollected = false; -- Don't spam the user twice
		wlScanGlyphs();
		wlPopupBox(WL_LIST_GLYPHS, wlScans.glyphs);

	elseif param1:match("^quests$") then
		wlScanQuests(true)

	else -- Show help
		for _,msg in ipairs(WL_HELP) do
			wlMessage(msg:gsub("%%VERSION%%", WL_VERSION), true);
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlFrameToggle(frame, msg, savename)
	wlSetting[savename] = not wlSetting[savename];

	if wlSetting[savename] then
		frame:Show();
	else
		frame:Hide();
	end

	wlMessage(WL_MESSAGE_TEMPLATE:format(WL_NAME, msg:format(wlEnabledDisabled(wlSetting[savename]))), true);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlFrameReset(frame)
	frame:ClearAllPoints();
	frame:SetPoint("CENTER");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlLocMapFrame_OnLoad()
	wlLocMapFrameText:SetText(UnitName("player") .. ": 100.0, 100.0  -  " .. WL_NA .. ": 100.0, 100.0");
	wlLocMapFrame:SetWidth(wlLocMapFrameText:GetStringWidth() + 20);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlLocMapFrame_OnUpdate()
-- Player
	local pX, pY = GetPlayerMapPosition("player");

	local playerStr = UnitName("player") .. ": |cffffffff";
	if pX == 0 or pY == 0 then
		playerStr = playerStr .. WL_NA;
	else
		playerStr = string.format("%s%.1f, %.1f", playerStr, pX * 100, pY * 100);
	end

	-- Cursor
	local width, height, scale = WorldMapDetailFrame:GetWidth(), WorldMapDetailFrame:GetHeight(), WorldMapDetailFrame:GetEffectiveScale();
	local cX, cY = WorldMapDetailFrame:GetCenter();
	local left, bottom = cX - width / 2, cY + height / 2;

	cX, cY = GetCursorPosition();
	cX, cY = (cX / scale - left) / width * 100, (bottom - cY / scale) / height * 100;

	local cursorStr = WL_LOC_CURSOR .. ": |cffffffff";
	if cX < 0 or cX > 100 or cY < 0 or cY > 100 then
		cursorStr = cursorStr .. WL_NA;
	else
		cursorStr = string.format("%s%.1f, %.1f", cursorStr, cX, cY);
	end

	wlLocMapFrameText:SetText(playerStr .. "|r  -  " .. cursorStr .. "|r");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEvent_ZONE_CHANGED()
	if (not WorldMapFrame or not WorldMapFrame:IsShown()) and (not BattlefieldMinimap or not BattlefieldMinimap:IsShown()) then
		SetMapToCurrentZone();
	end

	wlLocTooltipFrame_OnUpdate();
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlLocTooltipFrame_OnUpdate()
	local pX, pY = GetPlayerMapPosition("player");

	-- Hide tooltip
	if pX == 0 and pY == 0 then
		wlLocTooltipFrame:Hide();

	elseif wlSetting.locTooltip then
		wlLocTooltipFrameText:SetText(string.format("%.1f, %.1f", pX * 100, pY * 100));
		wlLocTooltipFrame:SetWidth(wlLocTooltipFrameText:GetStringWidth() + 14);

		wlLocTooltipFrame:Show();
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlIdTooltipFrame_OnUpdate()
	local name, id, kind = wlUnitName("npc"), wlUnitGUID("npc");

	if not name or not id then
		name, id, kind = wlUnitName("target"), wlUnitGUID("target");

		if not name or not id then
			name, id, kind = wlUnitName("mouseover"), wlUnitGUID("mouseover");

			if not name or not id then
				wlIdTooltipFrameText:SetText(WL_NA);
			end
		end
	end

	if name and id then
		wlIdTooltipFrameText:SetText(name .. "|n" .. WL_MESSAGE_TEMPLATE:format(kind, id));
	end

	wlIdTooltipFrame:SetWidth(wlIdTooltipFrameText:GetStringWidth() + 14);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlPopupBox(staticText, editText)
	wlPopupFrameCaption:SetText(staticText or "");
	wlPopupFrameEdit:SetText(editText or "");
	wlPopupFrame:Show();
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlMessage(msg, userInitiated, timedMsg, minDelay)
	if not msg then return end

	if userInitiated then
		local now = wlGetTime()
		local showMessage = true
		if timedMsg then
			if minDelay then
				if wlMessages[timedMsg] and now < wlMessages[timedMsg] then
					showMessage = false
				end
				wlMessages[timedMsg] = now + minDelay
			else
				wlMessages[timedMsg] = wlGetTime() + 1800000; -- 30 minutes
			end
		end

		if showMessage then DEFAULT_CHAT_FRAME:AddMessage(msg) end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlEnabledDisabled(good)
	if good then
		return WL_ENABLED;
	else
		return WL_DISABLED;
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlPrint(...)
	wlDebugEdit:SetText(wlDebugEdit:GetText() .. wlConcatToken(" : ", ...) .. "\n");
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlGlobalEvents = {};
function wlGlobalUnregister(eventName)
	if wlGlobalEvents[eventName] then
		return;
	end

	wlGlobalEvents[eventName] = { GetFramesRegisteredForEvent(eventName) };

	for _,frame in ipairs(wlGlobalEvents[eventName]) do
		frame:UnregisterEvent(eventName);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGlobalRegister(eventName)
	if type(wlGlobalEvents[eventName]) ~= "table" then
		return;
	end

	for _,frame in ipairs(wlGlobalEvents[eventName]) do
		frame:RegisterEvent(eventName);
	end

	wlGlobalEvents[eventName] = nil;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



--------------------------
--------------------------
--                      --
--   PARSE  FUNCTIONS   --
--                      --
--------------------------
--------------------------

-- |cff9d9d9d|Hitem:3170:0:0:0:0:0:0:467839504:80:0|h[Large Bear Tooth]|h|r : 5
-- |cff9d9d9d|Hitem:62328:0:0:0:0:0:0:488826832:80:0|h[Shed Fur]|h|r : 5

--	(color) : (id) : (enchant) : (1st socket) : (2nd socket) : (3rd socket) : (4th socket) : (subid) : (guid) : (playerLevel) : (??) : (name)
local WL_ITEMLINK = "|c(%x+)|Hitem:(%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+)|h%[(.+)%]|h|r";

function wlParseItemLink(link)
	if link then

		local found, _, color, id, enchant, socket1, socket2, socket3, socket4, subId, guid, pLevel, unk1, name = string.find(link, WL_ITEMLINK);

		if found then
			id, subId, guid = tonumber(id), tonumber(subId), tonumber(guid);

			if subId ~= 0 then
				wlUpdateVariable(wlItemSuffix, id, subId, "add", 1);

				if subId < 0 then
					wlUpdateVariable(wlItemSuffix, id, "sF", "set", bit.band(guid, 0xFFFF));
				end
			end

			return id, subId, tonumber(enchant), tonumber(socket1), tonumber(socket2), tonumber(socket3), tonumber(socket4), name, color, guid, tonumber(pLevel);
		end
	end

	return 0, 0, 0, 0, 0, 0, 0, "", "", 0, 0;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- |cffffff00|Hquest:10002:64|h[The Firewing Liaison]|h|r
-- |cffffff00|Hquest:11506:-1|h[Spirits of Auchindoun]|h|r
-- (color) : (id) : (level) : (name)
local WL_QUESTLINK = "|c(%x+)|Hquest:(%d+):(-?%d+)|h%[(.+)%]|h|r";

function wlParseQuestLink(link)
	if link then
		for color,id,level,name in string.gmatch(link, WL_QUESTLINK) do
			return tonumber(id), tonumber(level), name, color;
		end
	end

	return 0, 0, "", "";
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

-- (id) : (color) : (name)
local WL_SPELLLINK = "|Hspell:(%d+)|h|r|c(%x+)%[(.+)%]|r|h";

function wlParseSpellLink(link)
	if link then
		for id,color,name in string.gmatch(link, WL_SPELLLINK) do
			return tonumber(id), name, color;
		end
	end

	return 0, "", "";
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local WL_CURRENCY = {
	["1"] = string.gsub(COPPER_AMOUNT, "%%d ", ""),
	["100"] = string.gsub(SILVER_AMOUNT, "%%d ", ""),
	["10000"] = string.gsub(GOLD_AMOUNT, "%%d ", ""),
};

function wlParseCoin(strCoin)
	local coin = 0;
	for k,v in pairs(WL_CURRENCY) do
		local found, _, a = string.find(strCoin, "(%d+) " .. v);
		if found then
			coin = coin + a * tonumber(k);
		end
	end

	return coin;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlParseGUID(guid)
	if not guid then
		return;
	end

	local id, kind;

	if guid:match("^0x4") then
		if guid == UnitGUID("npc") then
			id, kind = wlParseItemLink(wlSelectOne(2, GetItemInfo(UnitName("npc")))), "item";
		else
			return nil, "item";
		end

	elseif string.match(guid, "^0xF.1") then
		id, kind = tonumber(string.sub(guid, 6, 10), 16), "object";

	elseif string.match(guid, "^0xF.[35]") then
		id, kind = tonumber(string.sub(guid, 6, 10), 16), "npc";
	end

	if id == 0 then
		id = nil;
	end

	return id, kind;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlParseString(template, str)
	if not template or not str then
		return nil;
	end

	template = string.gsub(template, "%%s", "(.+)");
	template = string.gsub(template, "%%d", "(%%d+)");

	return wlParseFind(string.find(str, template));
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlParseFind(found, _, ...)
	if not found then
		return nil;
	end

	return ...;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlQuestKeywords = nil;
function wlGetSourceText(str, nameOnly) -- @BUG@

	if GetLocale() == "ruRU" then
		return str;
	end

	-- Init
	if not wlQuestKeywords then
		wlQuestKeywords = {};

		-- Race
		local race = UnitRace("player");
		wlQuestKeywords[race] = "R" .. (wlQuestKeywords[race] or "");

		-- Class
		local class = UnitClass("player");
		wlQuestKeywords[class] = "C" .. (wlQuestKeywords[class] or "");

		-- Player name
		local name = UnitName("player");
		wlQuestKeywords[name] = "N" .. (wlQuestKeywords[name] or "");
	end

	if not str or str == "" then
		return "";
	end


	-- Mapping function
	local findNreplace = function(str, word, key)
	-- all string
		if str == word then
			return "$" .. key;
		end

		-- start of string
		str = str:gsub("^" .. word .. "([^%w])", "$" .. key .. "%1");

		-- middle of string
		str = str:gsub("([^%w])" .. word .. "([^%w])", "%1$" .. key .. "%2");

		-- end of string
		str = str:gsub("([^%w])" .. word .. "$", "%1$" .. key);

		return str;
	end;

	-- Scanning...
	for word,key in pairs(wlQuestKeywords) do
		if not nameOnly or key == "N" then
			str = findNreplace(str, word, key); -- Uppercase
			str = findNreplace(str, string.lower(word), string.lower(key)); -- Lowercase
		end
	end

	return str;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



----------------------------
----------------------------
--                        --
--   COMPUTE  FUNCTIONS   --
--                        --
----------------------------
----------------------------

local WL_OPS = {
	init =function(a, b) if type(a) ~= "nil" then return a; end return b; end, -- <false> is a valid init state
	initArray =function(a, _) if type(a) ~= "nil" then return a; end return {}; end, -- <false> is a valid init state
	set =function(_, b) return b; end,
	add =function(a, b) return (tonumber(a) or 0) + b; end,
	multiply =function(a, b) return (tonumber(a) or 1) * b; end,
	concat =function(a, b) return (a or "") .. b; end,
	min =function(a, b) a, b = tonumber(a), tonumber(b); return a and b and math.min(a, b) or a or b; end,
	max =function(a, b) a, b = tonumber(a), tonumber(b); return a and b and math.max(a, b) or a or b; end,
};

function wlUpdateVariable(master, first, ...)

	local nParams = select("#", ...);

	if not master or not first or nParams <= 1 then
		return nil;

	elseif nParams == 2 then
		if not WL_OPS[wlSelectOne(1, ...)] then
			return nil;
		end

		master[first] = WL_OPS[wlSelectOne(1, ...)](master[first], wlSelectOne(2, ...));
		return master[first];

	else
		if not master[first] then
			master[first] = {};
		end

		return wlUpdateVariable(master[first], ...);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlSelectOne(i, ...)
-- We only want the first returned value
	local v = select(i, ...);

	return v;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlRepDiscount = {
	[5] = 0.95, -- Friend:   5%
	[6] = 0.90, -- Honored: 10%
	[7] = 0.85, -- Revered: 15%
	[8] = 0.80, -- Exalted: 20%
};

function wlGetFullCost(cost, standing)
	if not cost then
		return 0;
	elseif not standing or not wlRepDiscount[standing] then
		return cost;
	end

	return math.floor(cost / wlRepDiscount[standing]);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableWalk(a, f)
	for k,v in pairs(a) do
		a[k] = f(k, v);
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableClear(a)
	wlTableWalk(a,function() return false; end);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlGetDate()
	return select(2, CalendarGetDate());
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local wlTimeOffset = nil;
function wlGetTime()
-- Init
	if not wlTimeOffset then
		local mo, d, y = wlGetDate();
		local h, mi = GetGameTime();

		wlTimeOffset = math.floor((time({ year = y, month = mo, day = d, hour = h, min = mi }) - GetTime()) * 1000);
	end

	return wlTimeOffset + math.floor(GetTime() * 1000);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlIsValidInterval(value1, value2, delta)
	return math.abs(value1 - value2) <= (wlSelectOne(3, GetNetStats()) + delta);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlIsEqualValues(value1, value2, delta)
	return math.abs(value1 - value2) <= delta;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

local UNKNOWNOBJECT, UKNOWNBEING, UNKNOWN = _G.UNKNOWNOBJECT:lower(), _G.UKNOWNBEING:lower(), _G.UNKNOWN:lower();
function wlIsValidName(name)
	return name and name ~= "" and name:lower() ~= UNKNOWNOBJECT and name:lower() ~= UKNOWNBEING and name:lower() ~= UNKNOWN;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlConcat(...)
	return wlConcatToken("^", ...);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlConcatToken(token, ...)
	local str = wlToString(wlSelectOne(1, ...));

	for i=2,select("#", ...) do
		str = str .. token .. wlToString(wlSelectOne(i, ...));
	end

	return str;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlToString(v)
	local typeV = type(v);

	if typeV == "table" or typeV == "function" or typeV == "thread" or typeV == "userdata" then
		return "<" .. typeV .. ">";

	elseif typeV == "boolean" then
		return v and "<true>" or "<false>";
	end

	return v or "<nil>";
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableCopy(Tr, Tx)
	for k,v in pairs(Tx) do
		Tr[k] = v;
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableFind(a, f, ...)
	for k,v2 in pairs(a) do
		if type(f) == "function" then
			if f(v2, ...) then
				return k;
			end
		else
			if f == v2 then
				return k;
			end
		end
	end

	for k,v2 in ipairs(a) do
		if type(f) == "function" then
			if f(v2, ...) then
				return k;
			end
		else
			if f == v2 then
				return k;
			end
		end
	end

	return nil;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTablePrint(t, indent)
	indent = indent or "";

	for k,v in pairs(t) do
		local typeK, typeV = type(k), type(v);

		if typeK == "number" then
			k = indent .. "[" .. k .. "]" .. " = ";
		else
			k = indent .. "[\"" .. k .. "\"]" .. " = ";
		end

		if typeV == "table" then
			wlPrint(k .. "{");
			wlTablePrint(v, indent .. "    ");
			wlPrint(indent .. "},");

		elseif typeV == "function" or typeV == "thread" or typeV == "userdata" then
			wlPrint(k .. "<" .. typeV .. "> ...,");

		elseif typeV == "boolean" then
			wlPrint(k .. "<boolean> " .. (v and "true" or "false") .. ",");

		elseif typeV == "string" then
			wlPrint(k .. "<string> \"" .. v .. "\",");

		else
			wlPrint(k .. "<" .. typeV .. "> " .. (v or k) .. ",");
		end
	end
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableCountValues(t)
	if "table" ~= type(t) then return end

	local count = 0
	for key,value in pairs(t) do
		if type(value) == "table" then
			count = count + wlTableCountValues(value)
		else
			count = count + 1
		end
	end

	return count
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableConcatValues(t, token)
	if "table" ~= type(t) then return end

	local text, first, temp = "", true;

	for key,value in pairs(t) do
		if type(value) == "table" then
			temp = wlTableConcatValues(value, token);
		else
			temp = value;
		end

		if temp and temp ~= "" then
			if first then
				first = false;
				text = temp;
			else
				text = text .. token .. temp;
			end
		end
	end

	return text;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableConcatKeys(t, token)
	local text, first, temp = "", true;

	for key,value in pairs(t) do
		if type(value) == "table" then
			temp = wlTableConcatKeys(value, token);
		else
			temp = key;
		end

		if temp and temp ~= "" then
			if first then
				first = false;
				text = temp;
			else
				text = text .. token .. temp;
			end
		end
	end

	return text;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableConcatKeyValues(t, token)
	local text, first, tempKey, tempValue = "", true;

	for key,value in pairs(t) do
		if type(value) == "table" then
			tempKey = wlTableConcatKeyValues(value, token);
			tempValue = nil;
		else
			tempKey = key;
			tempValue = value;
		end

		if tempValue and tempValue ~= "" and tempKey and tempKey ~= "" then
			if first then
				first = false;
				text = tempKey .. token .. tempValue;
			else
				text = text .. token .. tempKey .. token .. tempValue;
			end
		elseif tempKey and tempKey ~= "" then
			if first then
				first = false;
				text = tempKey;
			else
				text = text .. token .. tempKey;
			end
		end
	end

	return text;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTableIsEmpty(t)
	if type(t) == "table" then
		for _ in pairs(t) do
			return false;
		end
	end

	return true;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



-------------------------
-------------------------
--                     --
--   HOOK  FUNCTIONS   --
--                     --
-------------------------
-------------------------

function wlHook()
	wlDefaultChatFrame_DisplayTimePlayed = ChatFrame_DisplayTimePlayed;
	ChatFrame_DisplayTimePlayed = wlChatFrame_DisplayTimePlayed;

	wlDefaultGetQuestReward = GetQuestReward;
	GetQuestReward = wlGetQuestReward;

	hooksecurefunc("UseContainerItem", wlUseContainerItem);
	hooksecurefunc("PlaceAuctionBid", wlPlaceAuctionBid);
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlUnhook()
	ChatFrame_DisplayTimePlayed = wlDefaultChatFrame_DisplayTimePlayed;
	GetQuestReward = wlDefaultGetQuestReward;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--



--------------------------
--------------------------
--                      --
--   STATS  FUNCTIONS   --
--                      --
--------------------------
--------------------------

function wlReset()
	wlVersion, wlUploaded, wlStats, wlExportData, wlRealmList = WL_VERSION, 0, "", "", {};
	wlAuction, wlEvent, wlItemSuffix, wlObject, wlProfile, wlUnit = {}, {}, {}, {}, {}, {};
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlRegisterStats()
	local nDrops, nQuests, nCharacters, _ = 0, 0, 0;

	for _,a in pairs(wlEvent) do
		for _,b in pairs(a) do
			for _,c in pairs(b) do
				if c.what == "loot" then
					nDrops = nDrops + 1;
				elseif c.what == "quest" then
					nQuests = nQuests + 1;
				end
			end
		end
	end

	_, nCharacters = wlExportData:gsub(";", "");

	wlStats = wlConcat(wlArrayLength(wlUnit), wlArrayLength(wlObject), nQuests, nDrops, wlArrayLength(wlAuction, 2), nCharacters, GetLocale(), wlSelectOne(2, GetBuildInfo()));
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlArrayLength(array, depth)
	if not array then
		return 0;
	end

	local count = 0;

	depth = depth or 0;
	if depth > 0 then
		for _,sub in pairs(array) do
			count = count + wlArrayLength(sub, depth - 1);
		end
	else
		for _,_ in pairs(array) do
			count = count + 1;
		end
	end

	return count;
end

--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

function wlTest(...)
	wlMessage("DEBUG: TEST", true)
	wlScanArchaeology()
end
